From a72453ff5257e02d73d0caf6d516570bc7c9069b Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 9 Mar 2020 13:38:52 +0100 Subject: [PATCH 01/63] Run cargo-audit daily or when dependencies have changed (#22) * Run cargo-audit daily and when dependencies have changed See https://github.com/interchainio/tendermint-rs/pull/144#issuecomment-595322485 * Change actions/checkout back to v2 --- .github/workflows/audit.yaml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/audit.yaml b/.github/workflows/audit.yaml index 53d1675693..e82002aa2d 100644 --- a/.github/workflows/audit.yaml +++ b/.github/workflows/audit.yaml @@ -1,11 +1,25 @@ -name: Audit Check -on: [pull_request] +name: Security Audit +on: + pull_request: + paths: Cargo.lock + push: + branches: develop + paths: Cargo.lock + schedule: + - cron: '0 0 * * *' jobs: security_audit: + name: Security Audit runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - name: Cache cargo bin + uses: actions/cache@v1 + with: + path: ~/.cargo/bin + key: ${{ runner.os }}-cargo-audit-v0.11.2 - uses: actions-rs/audit-check@v1 with: + args: --ignore RUSTSEC-2019-0031 token: ${{ secrets.GITHUB_TOKEN }} From db92c7241ae6ea723bb4fbe1148de71f4f8a9493 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 9 Mar 2020 14:10:19 +0100 Subject: [PATCH 02/63] Implement QueryClientConsensusState IBC query (#23) Summary: Implement the `QueryClientConsensusState` IBC query, some of its dependencies, and a mostly type-safe IBC wrapper around `tendermint::rpc::Client`. Notes: - Comments/documentation is currently missing. - There is still a bunch of function/trait stubs with `todo!()` bodies. - Some functions contains calls to `unwrap()` which may panic. - Commitment proofs verification is not implemented. The related data structures are only stubs before we link against the [`ics23`](https://github.com/confio/ics23) crate. - Amino deserialization of the IBC query response value is not implemented. - This commit depends on some changes to the `tendermint-rs` crate, which are available in [my fork](https://github.com/romac/tendermint-rs/compare/rpc-client-new-sync). --- modules/Cargo.toml | 3 +- modules/src/ics02_client/client_type.rs | 9 ++ .../consensus.rs => consensus_state.rs} | 0 modules/src/ics07_tendermint/mod.rs | 2 +- .../ics07_tendermint/msgs/create_client.rs | 2 +- modules/src/ics23_commitment/mod.rs | 23 ++++ modules/src/lib.rs | 1 + modules/src/path.rs | 59 +++++++++ relayer/cli/Cargo.toml | 5 +- relayer/relay/Cargo.toml | 4 +- relayer/relay/src/chain.rs | 15 +++ relayer/relay/src/chain/tendermint.rs | 34 +++++ relayer/relay/src/error.rs | 3 + relayer/relay/src/lib.rs | 2 + relayer/relay/src/query.rs | 56 +++++++++ .../relay/src/query/client_consensus_state.rs | 117 ++++++++++++++++++ 16 files changed, 329 insertions(+), 6 deletions(-) rename modules/src/ics07_tendermint/{state/consensus.rs => consensus_state.rs} (100%) create mode 100644 modules/src/path.rs create mode 100644 relayer/relay/src/chain.rs create mode 100644 relayer/relay/src/chain/tendermint.rs create mode 100644 relayer/relay/src/query.rs create mode 100644 relayer/relay/src/query/client_consensus_state.rs diff --git a/modules/Cargo.toml b/modules/Cargo.toml index 8b13d07c9a..daf4ea02e5 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -10,7 +10,8 @@ authors = [ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tendermint = { git = "https://github.com/interchainio/tendermint-rs.git" } +tendermint = { git = "https://github.com/romac/tendermint-rs.git", branch = "rpc-client-new-sync" } + anomaly = "0.2.0" thiserror = "1.0.11" serde_derive = "1.0.104" diff --git a/modules/src/ics02_client/client_type.rs b/modules/src/ics02_client/client_type.rs index 91a15fdbeb..1be86c4a31 100644 --- a/modules/src/ics02_client/client_type.rs +++ b/modules/src/ics02_client/client_type.rs @@ -1,6 +1,8 @@ use super::error; use anomaly::fail; +pub struct Tendermint; + /// Type of the consensus algorithm #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ClientType { @@ -16,6 +18,12 @@ impl ClientType { } } +impl From for ClientType { + fn from(_: Tendermint) -> Self { + Self::Tendermint + } +} + impl std::str::FromStr for ClientType { type Err = error::Error; @@ -35,6 +43,7 @@ mod tests { #[test] fn parse_tendermint_client_type() { let client_type = ClientType::from_str("tendermint"); + match client_type { Ok(ClientType::Tendermint) => assert!(true), Err(_) => assert!(false, "parse failed"), diff --git a/modules/src/ics07_tendermint/state/consensus.rs b/modules/src/ics07_tendermint/consensus_state.rs similarity index 100% rename from modules/src/ics07_tendermint/state/consensus.rs rename to modules/src/ics07_tendermint/consensus_state.rs diff --git a/modules/src/ics07_tendermint/mod.rs b/modules/src/ics07_tendermint/mod.rs index 1b89ea68b2..2b656579fa 100644 --- a/modules/src/ics07_tendermint/mod.rs +++ b/modules/src/ics07_tendermint/mod.rs @@ -1,6 +1,6 @@ //! ICS 07: Tendermint Client +pub mod consensus_state; pub mod error; pub mod header; pub mod msgs; -pub mod state; diff --git a/modules/src/ics07_tendermint/msgs/create_client.rs b/modules/src/ics07_tendermint/msgs/create_client.rs index 1dab58eb5b..d7c085277b 100644 --- a/modules/src/ics07_tendermint/msgs/create_client.rs +++ b/modules/src/ics07_tendermint/msgs/create_client.rs @@ -1,7 +1,7 @@ use crate::ics02_client::client_type::ClientType; use crate::ics02_client::msgs::Msg; +use crate::ics07_tendermint::consensus_state::ConsensusState; use crate::ics07_tendermint::header::Header; -use crate::ics07_tendermint::state::consensus::ConsensusState; use crate::ics23_commitment::CommitmentRoot; use crate::ics24_host::client::ClientId; diff --git a/modules/src/ics23_commitment/mod.rs b/modules/src/ics23_commitment/mod.rs index e588362e82..b10701e993 100644 --- a/modules/src/ics23_commitment/mod.rs +++ b/modules/src/ics23_commitment/mod.rs @@ -1,4 +1,27 @@ use serde_derive::{Deserialize, Serialize}; +use crate::path::Path; + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct CommitmentRoot; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CommitmentPath; + +impl CommitmentPath { + pub fn from_path

(_p: P) -> Self + where + P: Path, + { + todo!() + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CommitmentProof; + +impl CommitmentProof { + pub fn from_bytes(_bytes: &[u8]) -> Self { + todo!() + } +} diff --git a/modules/src/lib.rs b/modules/src/lib.rs index 6cf7bd3b29..69162648d5 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -23,6 +23,7 @@ pub mod ics07_tendermint; pub mod ics23_commitment; pub mod ics24_host; pub mod keys; +pub mod path; /// Height of a block, same as in `tendermint` crate pub type Height = tendermint::lite::Height; diff --git a/modules/src/path.rs b/modules/src/path.rs new file mode 100644 index 0000000000..f4376235e5 --- /dev/null +++ b/modules/src/path.rs @@ -0,0 +1,59 @@ +use std::fmt; + +use crate::ics24_host::client::ClientId; +use crate::Height; + +pub struct Key<'a, P>(&'a P); + +impl<'a, P> fmt::Display for Key<'a, P> +where + P: Path, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.to_string()) + } +} + +impl<'a, P> Into> for Key<'a, P> +where + P: Path, +{ + fn into(self) -> Vec { + self.to_string().into_bytes() + } +} + +pub trait Path: Sized { + fn to_string(&self) -> String; + + fn to_key(&self) -> Key<'_, Self> { + Key(self) + } +} + +pub struct ConnectionPath { + pub connection_id: String, +} + +impl Path for ConnectionPath { + fn to_string(&self) -> String { + format!("connection/{}", self.connection_id) + } +} + +pub struct ConsensusStatePath { + pub client_id: ClientId, + pub height: Height, +} + +impl ConsensusStatePath { + pub fn new(client_id: ClientId, height: Height) -> Self { + Self { client_id, height } + } +} + +impl Path for ConsensusStatePath { + fn to_string(&self) -> String { + format!("consensusState/{}/{}", self.client_id, self.height) + } +} diff --git a/relayer/cli/Cargo.toml b/relayer/cli/Cargo.toml index e164c00009..7050e1d129 100644 --- a/relayer/cli/Cargo.toml +++ b/relayer/cli/Cargo.toml @@ -8,11 +8,12 @@ authors = [ ] [dependencies] +relayer = { path = "../relay" } + +anomaly = "0.2.0" gumdrop = "0.7" serde = { version = "1", features = ["serde_derive"] } thiserror = "1" -anomaly = "0.2.0" -relayer = { path = "../relay" } [dependencies.abscissa_core] version = "0.5.2" diff --git a/relayer/relay/Cargo.toml b/relayer/relay/Cargo.toml index 6a75899d5b..ec13a37fe1 100644 --- a/relayer/relay/Cargo.toml +++ b/relayer/relay/Cargo.toml @@ -10,7 +10,9 @@ authors = [ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tendermint = { git = "https://github.com/interchainio/tendermint-rs.git" } +relayer-modules = { path = "../../modules" } +tendermint = { git = "https://github.com/romac/tendermint-rs.git", branch = "rpc-client-new-sync" } + anomaly = "0.2.0" humantime-serde = "1.0.0" serde = "1.0.97" diff --git a/relayer/relay/src/chain.rs b/relayer/relay/src/chain.rs new file mode 100644 index 0000000000..95d5327598 --- /dev/null +++ b/relayer/relay/src/chain.rs @@ -0,0 +1,15 @@ +use ::tendermint::rpc; + +use relayer_modules::ics02_client::state::ConsensusState; + +use crate::config::ChainConfig; + +pub mod tendermint; + +pub trait Chain { + type Type; + type ConsensusState: ConsensusState; + + fn config(&self) -> &ChainConfig; + fn rpc_client(&self) -> &rpc::Client; // TODO: Define our own generic client interface? +} diff --git a/relayer/relay/src/chain/tendermint.rs b/relayer/relay/src/chain/tendermint.rs new file mode 100644 index 0000000000..5f28974be3 --- /dev/null +++ b/relayer/relay/src/chain/tendermint.rs @@ -0,0 +1,34 @@ +use tendermint::rpc; + +use relayer_modules::ics02_client::client_type::Tendermint; +use relayer_modules::ics07_tendermint::consensus_state::ConsensusState; + +use crate::config::ChainConfig; +use crate::error; + +use super::Chain; + +pub struct TendermintChain { + config: ChainConfig, + rpc_client: rpc::Client, +} + +impl TendermintChain { + pub fn from_config(config: ChainConfig) -> Result { + let rpc_client = rpc::Client::new(config.rpc_addr.clone()); + Ok(Self { config, rpc_client }) + } +} + +impl Chain for TendermintChain { + type Type = Tendermint; + type ConsensusState = ConsensusState; + + fn config(&self) -> &ChainConfig { + &self.config + } + + fn rpc_client(&self) -> &rpc::Client { + &self.rpc_client + } +} diff --git a/relayer/relay/src/error.rs b/relayer/relay/src/error.rs index 96a631d52c..42c045d0b3 100644 --- a/relayer/relay/src/error.rs +++ b/relayer/relay/src/error.rs @@ -16,6 +16,9 @@ pub enum Kind { /// Invalid configuration #[error("invalid configuration")] Config, + + #[error("RPC error")] + Rpc, } impl Kind { diff --git a/relayer/relay/src/lib.rs b/relayer/relay/src/lib.rs index 1e1b978712..fdf18567e8 100644 --- a/relayer/relay/src/lib.rs +++ b/relayer/relay/src/lib.rs @@ -11,5 +11,7 @@ //! IBC Relayer implementation +pub mod chain; pub mod config; pub mod error; +pub mod query; diff --git a/relayer/relay/src/query.rs b/relayer/relay/src/query.rs new file mode 100644 index 0000000000..634d6e6871 --- /dev/null +++ b/relayer/relay/src/query.rs @@ -0,0 +1,56 @@ +use tendermint::abci; +use tendermint::rpc::{self, endpoint::abci_query::AbciQuery}; + +use relayer_modules::Height; + +use crate::chain::Chain; + +mod client_consensus_state; +pub use client_consensus_state::*; + +pub trait IbcResponse: Sized {} + +pub trait IbcQuery { + type Response: IbcResponse; + + fn path(&self) -> abci::Path; + fn height(&self) -> Height; + fn prove(&self) -> bool; + fn data(&self) -> Vec; +} + +pub async fn ibc_query(chain: &C, query: Q) -> Result +where + C: Chain, + Q: IbcQuery, +{ + let abci_response = chain + .rpc_client() + .abci_query( + Some(query.path()), + query.data(), + Some(query.height().into()), + query.prove(), + ) + .await?; + + if !abci_response.code.is_ok() { + todo!() // fail with response log + } + + // Data from trusted node or subspace query doesn't need verification + if !is_query_store_with_proof(&query.path()) { + return Ok(abci_response); + } + + // TODO: Verify proof and return response + Ok(abci_response) +} + +/// Whether or not this path requires proof verification. +/// +/// is_query_store_with_proofxpects a format like ///, +/// where queryType must be "store" and subpath must be "key" to require a proof. +fn is_query_store_with_proof(_path: &abci::Path) -> bool { + todo!() +} diff --git a/relayer/relay/src/query/client_consensus_state.rs b/relayer/relay/src/query/client_consensus_state.rs new file mode 100644 index 0000000000..63cd890b8b --- /dev/null +++ b/relayer/relay/src/query/client_consensus_state.rs @@ -0,0 +1,117 @@ +use std::marker::PhantomData; + +use tendermint::abci; + +use relayer_modules::ics02_client::state::ConsensusState; +use relayer_modules::ics23_commitment::{CommitmentPath, CommitmentProof}; +use relayer_modules::ics24_host::client::ClientId; +use relayer_modules::path::{ConsensusStatePath, Path}; +use relayer_modules::Height; + +use super::{ibc_query, IbcQuery, IbcResponse}; +use crate::chain::Chain; +use crate::error; + +pub struct QueryClientConsensusState { + pub height: Height, + pub consensus_state_path: ConsensusStatePath, + marker: PhantomData, +} + +impl QueryClientConsensusState { + pub fn new(height: Height, client_id: ClientId) -> Self { + Self { + height, + consensus_state_path: ConsensusStatePath { client_id, height }, + marker: PhantomData, + } + } +} + +impl IbcQuery for QueryClientConsensusState +where + CS: ConsensusState, +{ + type Response = ConsensusStateResponse; + + fn path(&self) -> abci::Path { + "/store/ibc/key".parse().unwrap() + } + + fn height(&self) -> Height { + self.height + } + + fn prove(&self) -> bool { + true + } + + fn data(&self) -> Vec { + self.consensus_state_path.to_key().into() + } +} + +pub struct ConsensusStateResponse { + pub consensus_state: CS, + pub proof: CommitmentProof, + pub proof_path: CommitmentPath, + pub proof_height: Height, +} + +impl ConsensusStateResponse { + pub fn new( + client_id: ClientId, + consensus_state: CS, + abci_proof: abci::Proof, + proof_height: Height, + ) -> Self { + let proof = CommitmentProof::from_bytes(abci_proof.as_ref()); + let proof_path = + CommitmentPath::from_path(ConsensusStatePath::new(client_id, proof_height)); + + ConsensusStateResponse { + consensus_state, + proof, + proof_path, + proof_height, + } + } +} + +impl IbcResponse for ConsensusStateResponse +where + CS: ConsensusState, +{ + // fn from_bytes(_bytes: &[u8]) -> Result { + // todo!() + // } +} + +pub async fn query_client_consensus_state( + chain: &C, + client_id: ClientId, + height: Height, +) -> Result, error::Error> +where + C: Chain, +{ + let query = QueryClientConsensusState::::new(height, client_id.clone()); + + let response = ibc_query(chain, query) + .await + .map_err(|e| error::Kind::Rpc.context(e))?; + + // FIXME: Handle case where there is no value + let consensus_state = amino_unmarshal_binary_length_prefixed(&response.value.unwrap())?; + + Ok(ConsensusStateResponse::new( + client_id, + consensus_state, + response.proof.unwrap(), // FIXME: Handle case where there is no proof + response.height.into(), + )) +} + +fn amino_unmarshal_binary_length_prefixed(_bytes: &[u8]) -> Result { + todo!() +} From 172220ea84b1a980d482b9ad8a7cd4647da90db9 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 11 Mar 2020 10:23:16 +0100 Subject: [PATCH 03/63] Extract IBC queries paths into conditional modules (#24) * Extract IBC queries paths into conditional modules Since these paths currently differ between the Go implementation and the ICS, we need to be able to switch them up at compile-time for when we will need to test against the Go implementation. * Revert tendermint dependency to master --- modules/Cargo.toml | 9 +++++++-- modules/src/path.rs | 12 ++++++++++-- modules/src/path/cosmos.rs | 9 +++++++++ modules/src/path/ics.rs | 9 +++++++++ relayer/relay/Cargo.toml | 2 +- 5 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 modules/src/path/cosmos.rs create mode 100644 modules/src/path/ics.rs diff --git a/modules/Cargo.toml b/modules/Cargo.toml index daf4ea02e5..a199c3a6ed 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -7,10 +7,15 @@ authors = [ "Romain Ruetschi " ] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +# Default features +default = ["paths-cosmos"] + +# In IBC queries, use paths as defined in the Cosmos-SDK Go implementation, rather than in the ICS. +paths-cosmos = [] [dependencies] -tendermint = { git = "https://github.com/romac/tendermint-rs.git", branch = "rpc-client-new-sync" } +tendermint = { git = "https://github.com/interchainio/tendermint-rs.git" } anomaly = "0.2.0" thiserror = "1.0.11" diff --git a/modules/src/path.rs b/modules/src/path.rs index f4376235e5..065ea674d4 100644 --- a/modules/src/path.rs +++ b/modules/src/path.rs @@ -3,6 +3,14 @@ use std::fmt; use crate::ics24_host::client::ClientId; use crate::Height; +mod cosmos; +mod ics; + +#[cfg(feature = "paths-cosmos")] +use cosmos as paths; +#[cfg(not(feature = "paths-cosmos"))] +use ics as paths; + pub struct Key<'a, P>(&'a P); impl<'a, P> fmt::Display for Key<'a, P> @@ -37,7 +45,7 @@ pub struct ConnectionPath { impl Path for ConnectionPath { fn to_string(&self) -> String { - format!("connection/{}", self.connection_id) + paths::connection_path(&self) } } @@ -54,6 +62,6 @@ impl ConsensusStatePath { impl Path for ConsensusStatePath { fn to_string(&self) -> String { - format!("consensusState/{}/{}", self.client_id, self.height) + paths::consensus_state_path(&self) } } diff --git a/modules/src/path/cosmos.rs b/modules/src/path/cosmos.rs new file mode 100644 index 0000000000..bcac757bcf --- /dev/null +++ b/modules/src/path/cosmos.rs @@ -0,0 +1,9 @@ +use super::{ConnectionPath, ConsensusStatePath}; + +pub fn connection_path(path: &ConnectionPath) -> String { + format!("connection/{}", path.connection_id) +} + +pub fn consensus_state_path(path: &ConsensusStatePath) -> String { + format!("consensusState/{}/{}", path.client_id, path.height) +} diff --git a/modules/src/path/ics.rs b/modules/src/path/ics.rs new file mode 100644 index 0000000000..42687fc711 --- /dev/null +++ b/modules/src/path/ics.rs @@ -0,0 +1,9 @@ +use super::{ConnectionPath, ConsensusStatePath}; + +pub fn connection_path(_path: &ConnectionPath) -> String { + todo!() +} + +pub fn consensus_state_path(_path: &ConsensusStatePath) -> String { + todo!() +} diff --git a/relayer/relay/Cargo.toml b/relayer/relay/Cargo.toml index ec13a37fe1..9e41e64b68 100644 --- a/relayer/relay/Cargo.toml +++ b/relayer/relay/Cargo.toml @@ -11,7 +11,7 @@ authors = [ [dependencies] relayer-modules = { path = "../../modules" } -tendermint = { git = "https://github.com/romac/tendermint-rs.git", branch = "rpc-client-new-sync" } +tendermint = { git = "https://github.com/interchainio/tendermint-rs.git" } anomaly = "0.2.0" humantime-serde = "1.0.0" From c67f1fcf1b876c0e6d9565106e32bfbb050d791f Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 11 Mar 2020 10:38:33 +0100 Subject: [PATCH 04/63] Improve IBC query API (#25) * Refactor IbcQuery and IbcResponse to provide ibc_query with a better signature * Remove associated type from IbcResponse trait --- relayer/relay/src/query.rs | 35 +++++++++------ .../relay/src/query/client_consensus_state.rs | 44 +++++++++++-------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/relayer/relay/src/query.rs b/relayer/relay/src/query.rs index 634d6e6871..14a930ad34 100644 --- a/relayer/relay/src/query.rs +++ b/relayer/relay/src/query.rs @@ -1,25 +1,32 @@ use tendermint::abci; -use tendermint::rpc::{self, endpoint::abci_query::AbciQuery}; +use tendermint::rpc::endpoint::abci_query::AbciQuery; use relayer_modules::Height; use crate::chain::Chain; +use crate::error; -mod client_consensus_state; -pub use client_consensus_state::*; +pub mod client_consensus_state; -pub trait IbcResponse: Sized {} +pub trait IbcResponse: Sized { + fn from_abci_response(query: Query, response: AbciQuery) -> Result; +} -pub trait IbcQuery { - type Response: IbcResponse; +pub trait IbcQuery: Sized { + type Response: IbcResponse; fn path(&self) -> abci::Path; fn height(&self) -> Height; fn prove(&self) -> bool; fn data(&self) -> Vec; + + fn build_response(self, response: AbciQuery) -> Result { + Self::Response::from_abci_response(self, response) + } } -pub async fn ibc_query(chain: &C, query: Q) -> Result +/// Perform an IBC `query` on the given `chain`, and return the corresponding IBC response. +pub async fn ibc_query(chain: &C, query: Q) -> Result where C: Chain, Q: IbcQuery, @@ -32,19 +39,19 @@ where Some(query.height().into()), query.prove(), ) - .await?; + .await + .map_err(|e| error::Kind::Rpc.context(e))?; if !abci_response.code.is_ok() { - todo!() // fail with response log + todo!() // TODO: Fail with response log } - // Data from trusted node or subspace query doesn't need verification - if !is_query_store_with_proof(&query.path()) { - return Ok(abci_response); + // Data that is not from trusted node or subspace query needs verification + if is_query_store_with_proof(&query.path()) { + todo!() // TODO: Verify proof } - // TODO: Verify proof and return response - Ok(abci_response) + query.build_response(abci_response) } /// Whether or not this path requires proof verification. diff --git a/relayer/relay/src/query/client_consensus_state.rs b/relayer/relay/src/query/client_consensus_state.rs index 63cd890b8b..d6bb098043 100644 --- a/relayer/relay/src/query/client_consensus_state.rs +++ b/relayer/relay/src/query/client_consensus_state.rs @@ -1,4 +1,5 @@ use std::marker::PhantomData; +use tendermint::rpc::endpoint::abci_query::AbciQuery; use tendermint::abci; @@ -14,6 +15,7 @@ use crate::error; pub struct QueryClientConsensusState { pub height: Height, + pub client_id: ClientId, pub consensus_state_path: ConsensusStatePath, marker: PhantomData, } @@ -22,7 +24,8 @@ impl QueryClientConsensusState { pub fn new(height: Height, client_id: ClientId) -> Self { Self { height, - consensus_state_path: ConsensusStatePath { client_id, height }, + client_id: client_id.clone(), + consensus_state_path: ConsensusStatePath::new(client_id, height), marker: PhantomData, } } @@ -78,13 +81,28 @@ impl ConsensusStateResponse { } } -impl IbcResponse for ConsensusStateResponse +impl IbcResponse> for ConsensusStateResponse where CS: ConsensusState, { - // fn from_bytes(_bytes: &[u8]) -> Result { - // todo!() - // } + fn from_abci_response( + query: QueryClientConsensusState, + response: AbciQuery, + ) -> Result { + match (response.value, response.proof) { + (Some(value), Some(proof)) => { + let consensus_state = amino_unmarshal_binary_length_prefixed(&value)?; + + Ok(ConsensusStateResponse::new( + query.client_id, + consensus_state, + proof, + response.height.into(), + )) + } + _ => todo!(), + } + } } pub async fn query_client_consensus_state( @@ -95,21 +113,9 @@ pub async fn query_client_consensus_state( where C: Chain, { - let query = QueryClientConsensusState::::new(height, client_id.clone()); - - let response = ibc_query(chain, query) - .await - .map_err(|e| error::Kind::Rpc.context(e))?; - - // FIXME: Handle case where there is no value - let consensus_state = amino_unmarshal_binary_length_prefixed(&response.value.unwrap())?; + let query = QueryClientConsensusState::new(height, client_id); - Ok(ConsensusStateResponse::new( - client_id, - consensus_state, - response.proof.unwrap(), // FIXME: Handle case where there is no proof - response.height.into(), - )) + ibc_query(chain, query).await } fn amino_unmarshal_binary_length_prefixed(_bytes: &[u8]) -> Result { From e39e84d9e03aff35b2b8ee7fbc6d1617d2b9fdde Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 17 Mar 2020 13:48:19 +0100 Subject: [PATCH 05/63] Fix ClientConsensusState query (#31) Closes: #30 Query of client consensus now distinguishes between query height and consensus height. Specifically: - `chain_height` is the height at which the query is made (can be 0 if the latest state on chain is queried). - `consensus_height` is the consensus height of the client running on this chain but it represents a height of the client's chain and should be used to construct the query path. --- .../relay/src/query/client_consensus_state.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/relayer/relay/src/query/client_consensus_state.rs b/relayer/relay/src/query/client_consensus_state.rs index d6bb098043..c2209a24b5 100644 --- a/relayer/relay/src/query/client_consensus_state.rs +++ b/relayer/relay/src/query/client_consensus_state.rs @@ -14,18 +14,20 @@ use crate::chain::Chain; use crate::error; pub struct QueryClientConsensusState { - pub height: Height, + pub chain_height: Height, pub client_id: ClientId, + pub consensus_height: Height, pub consensus_state_path: ConsensusStatePath, marker: PhantomData, } impl QueryClientConsensusState { - pub fn new(height: Height, client_id: ClientId) -> Self { + pub fn new(chain_height: Height, client_id: ClientId, consensus_height: Height) -> Self { Self { - height, + chain_height, client_id: client_id.clone(), - consensus_state_path: ConsensusStatePath::new(client_id, height), + consensus_height, + consensus_state_path: ConsensusStatePath::new(client_id, consensus_height), marker: PhantomData, } } @@ -42,7 +44,7 @@ where } fn height(&self) -> Height { - self.height + self.chain_height } fn prove(&self) -> bool { @@ -69,6 +71,7 @@ impl ConsensusStateResponse { proof_height: Height, ) -> Self { let proof = CommitmentProof::from_bytes(abci_proof.as_ref()); + let proof_path = CommitmentPath::from_path(ConsensusStatePath::new(client_id, proof_height)); @@ -107,13 +110,14 @@ where pub async fn query_client_consensus_state( chain: &C, + chain_height: Height, client_id: ClientId, - height: Height, + consensus_height: Height, ) -> Result, error::Error> where C: Chain, { - let query = QueryClientConsensusState::new(height, client_id); + let query = QueryClientConsensusState::new(chain_height, client_id, consensus_height); ibc_query(chain, query).await } From de418e339db87fc4f52cf005f2388f24191ff261 Mon Sep 17 00:00:00 2001 From: Anca Zamfir Date: Thu, 19 Mar 2020 16:05:44 +0100 Subject: [PATCH 06/63] Update PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8590275a74..d3873de643 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -17,7 +17,8 @@ ______ For contributor use: -- [ ] Wrote tests +- [ ] Unit tests written +- [ ] Added test to CI if applicable - [ ] Updated CHANGELOG_PENDING.md - [ ] Linked to Github issue with discussion and accepted design OR link to spec that describes this work. - [ ] Updated relevant documentation (`docs/`) and code comments From 96ee56d7b1c731c982f8085b9441e62cce974c00 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Fri, 20 Mar 2020 11:50:52 +0100 Subject: [PATCH 07/63] Add commands to initialize light client for given chain and start verifying headers (#26) * Add function to initialize lite client without trusted state * Make LightClient generic over chain type via a generic store * Add stub to initialize the light client with trust options * Use custom sum type for store heights * Rename LiteClient to LightClient * Add stub command `light init` * Implement LightClient::init_with_node_trusted_state * Implement LightClient::init_with_trusted_state * Refactor light client * Verify trusted state on light client initialization * Remove unused file * Add stub for Client::check_trusted_header * Fail when needed in Client::update_trusted_state * Partially implement Client::update * Implement LightClient::verify_header * Update comment * Fix clippy warnings * Use serde-humantime to parse trusting_period * Move config defaults into their own module * Create light client and display last trusted state * Use checked arithmetic when incrementing height * Update trusted store in Client::update * Fix clippy warnings * Rename StoreHeight:GivenHeight to Given * Simplify verify_header signature * Spawn empty relayer, and one client per configured chain * Update tendermint-rs repository * Remove dep on tendermint-rs/light_node by copying RpcRequester code over * Improve reporting a bit * Fix RpcRequester unit test * Add persistent trusted store implementation * Use persistent trusted store in commands * Ignore database folders in Git * Fix clippy warnings * Remove superfluous Tendermint type struct * Add some doc comments * Document the relayer::client::Client struct * More doc comments * Ignore .db and .sh files in cli folder * Fix misleading doc comment * Update README and LICENSE file * Remove verbose flag in README * Add status info to `light init` command --- .gitignore | 2 + LICENSE | 4 +- README.md | 49 ++- modules/Cargo.toml | 2 +- modules/src/ics02_client/client_type.rs | 8 - relayer/cli/.gitignore | 3 + relayer/cli/Cargo.toml | 3 + relayer/cli/src/application.rs | 6 +- relayer/cli/src/commands.rs | 13 +- relayer/cli/src/commands/light.rs | 13 + relayer/cli/src/commands/light/init.rs | 118 ++++++ relayer/cli/src/commands/start.rs | 105 ++++- relayer/relay/Cargo.toml | 10 +- relayer/relay/src/chain.rs | 86 +++- relayer/relay/src/chain/tendermint.rs | 41 +- relayer/relay/src/client.rs | 397 ++++++++++++++++++ relayer/relay/src/client/rpc_requester.rs | 73 ++++ relayer/relay/src/client/trust_options.rs | 41 ++ relayer/relay/src/config.rs | 43 +- relayer/relay/src/error.rs | 17 + relayer/relay/src/lib.rs | 4 +- relayer/relay/src/query.rs | 13 + relayer/relay/src/store.rs | 72 ++++ relayer/relay/src/store/mem.rs | 80 ++++ relayer/relay/src/store/sled.rs | 138 ++++++ .../config/fixtures/relayer_conf_example.toml | 5 +- 26 files changed, 1284 insertions(+), 62 deletions(-) create mode 100644 relayer/cli/src/commands/light.rs create mode 100644 relayer/cli/src/commands/light/init.rs create mode 100644 relayer/relay/src/client.rs create mode 100644 relayer/relay/src/client/rpc_requester.rs create mode 100644 relayer/relay/src/client/trust_options.rs create mode 100644 relayer/relay/src/store.rs create mode 100644 relayer/relay/src/store/mem.rs create mode 100644 relayer/relay/src/store/sled.rs diff --git a/.gitignore b/.gitignore index 671ef89996..bf39ab9621 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk +# Ignore database folders +**/*.db/ diff --git a/LICENSE b/LICENSE index 7a4a3ea242..58b61ab0f6 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2020 Informal Systems Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -199,4 +199,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/README.md b/README.md index 6ea960afe0..542455429f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ # ibc-rs -Rust implementation of IBC modules and relayer + +> Rust implementation of IBC modules and relayer + +## Disclaimer + +THIS PROJECT IS UNDER HEAVY DEVELOPMENT AND IS NOT IN A WORKING STAGE NOW, USE AT YOUR OWN RISK. + +## Requirements + +- Rust 1.42+ (might work on earlier versions but this has not been tested yet) + +## Usage + +Provided that one has a Tendermint node running on port 26657, one can +configure and spawn two light clients for this chain with the following commands: + +1. Fetch a trusted header from the chain: + +```bash +$ curl -s http://localhost:26657/status | jq '.result.sync_info|.latest_block_hash,.latest_block_height' +``` + +2. Initialize a light client for chain A with the trusted height and hash fetched in step 1: + +```bash +ibc-rs > cargo run --bin relayer -- -c ./relayer/relay/tests/config/fixtures/relayer_conf_example.toml light init -x HASH -h HEIGHT chain_A +``` +> Replace `HASH` and `HEIGHT` with the appropriate values. + +3. Repeat step 1 and 2 above for `chain_B`. + +> For this, update the height and hash, and change the chain identifier in the command above from `chain_A` to `chain_B`. + +4. Start the light clients and a dummy relayer thread: + +```bash +ibc-rs > cargo run --bin relayer -- -c ./relayer/relay/tests/config/fixtures/relayer_conf_example.toml run +``` + +## License + +Copyright © 2020 Informal Systems + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/modules/Cargo.toml b/modules/Cargo.toml index a199c3a6ed..ce73546610 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -15,7 +15,7 @@ default = ["paths-cosmos"] paths-cosmos = [] [dependencies] -tendermint = { git = "https://github.com/interchainio/tendermint-rs.git" } +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git" } anomaly = "0.2.0" thiserror = "1.0.11" diff --git a/modules/src/ics02_client/client_type.rs b/modules/src/ics02_client/client_type.rs index 1be86c4a31..67d1a3fb65 100644 --- a/modules/src/ics02_client/client_type.rs +++ b/modules/src/ics02_client/client_type.rs @@ -1,8 +1,6 @@ use super::error; use anomaly::fail; -pub struct Tendermint; - /// Type of the consensus algorithm #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ClientType { @@ -18,12 +16,6 @@ impl ClientType { } } -impl From for ClientType { - fn from(_: Tendermint) -> Self { - Self::Tendermint - } -} - impl std::str::FromStr for ClientType { type Err = error::Error; diff --git a/relayer/cli/.gitignore b/relayer/cli/.gitignore index 53eaa21960..27128c443f 100644 --- a/relayer/cli/.gitignore +++ b/relayer/cli/.gitignore @@ -1,2 +1,5 @@ /target **/*.rs.bk + +*.sh +*.db diff --git a/relayer/cli/Cargo.toml b/relayer/cli/Cargo.toml index 7050e1d129..d62d826cb5 100644 --- a/relayer/cli/Cargo.toml +++ b/relayer/cli/Cargo.toml @@ -9,11 +9,14 @@ authors = [ [dependencies] relayer = { path = "../relay" } +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git" } anomaly = "0.2.0" gumdrop = "0.7" serde = { version = "1", features = ["serde_derive"] } thiserror = "1" +abscissa_tokio = "0.5.1" +tokio = "0.2.13" [dependencies.abscissa_core] version = "0.5.2" diff --git a/relayer/cli/src/application.rs b/relayer/cli/src/application.rs index 25cbc278e8..9d7ff6ac43 100644 --- a/relayer/cli/src/application.rs +++ b/relayer/cli/src/application.rs @@ -82,7 +82,11 @@ impl Application for CliApp { /// beyond the default ones provided by the framework, this is the place /// to do so. fn register_components(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> { - let components = self.framework_components(command)?; + use abscissa_tokio::TokioComponent; + + let mut components = self.framework_components(command)?; + components.push(Box::new(TokioComponent::new()?)); + self.state.components.register(components) } diff --git a/relayer/cli/src/commands.rs b/relayer/cli/src/commands.rs index 03d7b9d9b4..f1f7374a75 100644 --- a/relayer/cli/src/commands.rs +++ b/relayer/cli/src/commands.rs @@ -6,10 +6,11 @@ //! application's configuration file. mod config; +mod light; mod start; mod version; -use self::{config::ConfigCmd, start::StartCmd, version::VersionCmd}; +use self::{config::ConfigCmd, light::LightCmd, start::StartCmd, version::VersionCmd}; use crate::config::Config; use abscissa_core::{Command, Configurable, FrameworkError, Help, Options, Runnable}; @@ -25,6 +26,10 @@ pub enum CliCmd { #[options(help = "get usage information")] Help(Help), + /// The `version` subcommand + #[options(help = "display version information")] + Version(VersionCmd), + /// The `start` subcommand #[options(help = "start the relayer")] Start(StartCmd), @@ -33,9 +38,9 @@ pub enum CliCmd { #[options(help = "manipulate the relayer configuration")] Config(ConfigCmd), - /// The `version` subcommand - #[options(help = "display version information")] - Version(VersionCmd), + /// The `light` subcommand + #[options(help = "basic functionality for managing the lite clients")] + Light(LightCmd), } /// This trait allows you to define how application configuration is loaded. diff --git a/relayer/cli/src/commands/light.rs b/relayer/cli/src/commands/light.rs new file mode 100644 index 0000000000..1c2cfbb846 --- /dev/null +++ b/relayer/cli/src/commands/light.rs @@ -0,0 +1,13 @@ +//! `light` subcommand + +use abscissa_core::{Command, Options, Runnable}; + +mod init; + +/// `light` subcommand +#[derive(Command, Debug, Options, Runnable)] +pub enum LightCmd { + /// The `light init` subcommand + #[options(help = "initiate a light client for a given chain")] + Init(init::InitCmd), +} diff --git a/relayer/cli/src/commands/light/init.rs b/relayer/cli/src/commands/light/init.rs new file mode 100644 index 0000000000..c099395ba9 --- /dev/null +++ b/relayer/cli/src/commands/light/init.rs @@ -0,0 +1,118 @@ +use std::future::Future; + +// use crate::application::APPLICATION; +use crate::prelude::*; + +use abscissa_core::{Command, Options, Runnable}; + +use tendermint::chain::Id as ChainId; +use tendermint::hash::Hash; +use tendermint::lite::Height; + +use relayer::chain::tendermint::TendermintChain; +use relayer::client::trust_options::TrustOptions; +use relayer::config::{ChainConfig, Config}; +use relayer::store::{sled::SledStore, Store}; + +#[derive(Command, Debug, Options)] +pub struct InitCmd { + #[options(free, help = "identifier of the chain to initialize light client for")] + chain_id: Option, + + #[options(help = "trusted header hash", short = "x")] + hash: Option, + + #[options(help = "trusted header height", short = "h")] + height: Option, +} + +#[derive(Clone, Debug)] +struct InitOptions { + /// identifier of chain to initialize light client for + chain_id: ChainId, + + /// trusted header hash + trusted_hash: Hash, + + /// trusted header height + trusted_height: Height, +} + +impl InitCmd { + fn get_chain_config_and_options( + &self, + config: &Config, + ) -> Result<(ChainConfig, InitOptions), String> { + match (&self.chain_id, &self.hash, self.height) { + (Some(chain_id), Some(trusted_hash), Some(trusted_height)) => { + let chain_config = config.chains.iter().find(|c| c.id == *chain_id); + + match chain_config { + Some(chain_config) => { + let opts = InitOptions { + chain_id: *chain_id, + trusted_hash: *trusted_hash, + trusted_height, + }; + + Ok((chain_config.clone(), opts)) + } + None => Err(format!("cannot find chain {} in config", chain_id)), + } + } + + (None, _, _) => Err("missing chain identifier".to_string()), + (_, None, _) => Err("missing trusted hash".to_string()), + (_, _, None) => Err("missing trusted height".to_string()), + } + } +} + +impl Runnable for InitCmd { + /// Initialize the light client for the given chain + fn run(&self) { + // FIXME: This just hangs and never runs the given future + // abscissa_tokio::run(&APPLICATION, ...).unwrap(); + + let config = app_config(); + + let (chain_config, opts) = match self.get_chain_config_and_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + + block_on(async { + let trust_options = TrustOptions::new( + opts.trusted_hash, + opts.trusted_height, + chain_config.trusting_period, + Default::default(), + ) + .unwrap(); + + let mut store: SledStore = + relayer::store::persistent(format!("store_{}.db", chain_config.id)); + + store.set_trust_options(trust_options).unwrap(); // FIXME: unwrap + + status_ok!( + chain_config.id, + "Set trusted options: hash={} height={}", + opts.trusted_hash, + opts.trusted_height + ); + }); + } +} + +fn block_on(future: F) -> F::Output { + tokio::runtime::Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap() + .block_on(future) +} diff --git a/relayer/cli/src/commands/start.rs b/relayer/cli/src/commands/start.rs index 3e6593acb2..ff21ebdeee 100644 --- a/relayer/cli/src/commands/start.rs +++ b/relayer/cli/src/commands/start.rs @@ -1,24 +1,105 @@ -//! `start` subcommand +use std::future::Future; +use std::time::{Duration, SystemTime}; -/// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` -/// accessors along with logging macros. Customize as you see fit. +// use crate::application::APPLICATION; use crate::prelude::*; use abscissa_core::{Command, Options, Runnable}; -/// `start` subcommand -/// -/// The `Options` proc macro generates an option parser based on the struct -/// definition, and is defined in the `gumdrop` crate. See their documentation -/// for a more comprehensive example: -/// -/// +use tendermint::lite::types::Header; + +use relayer::chain::tendermint::TendermintChain; +use relayer::chain::Chain; +use relayer::client::Client; +use relayer::config::ChainConfig; +use relayer::store::Store; + #[derive(Command, Debug, Options)] pub struct StartCmd {} impl Runnable for StartCmd { - /// Start the application. fn run(&self) { - status_ok!("{}", "Quitting..."); + let config = app_config().clone(); + + // FIXME: This just hangs and never runs the given future + // abscissa_tokio::run(&APPLICATION, ...).unwrap(); + + block_on(async { + for chain_config in config.chains { + status_info!( + "Relayer", + "Spawning light client for chain {}", + chain_config.id + ); + + let _handle = tokio::spawn(async move { + let client = create_client(chain_config).await; + let trusted_state = client.last_trusted_state().unwrap(); + + status_ok!( + client.chain().id(), + "Spawned new client now at trusted state: {} at height {}", + trusted_state.last_header().header().hash(), + trusted_state.last_header().header().height(), + ); + + update_headers(client).await; + }); + } + + start_relayer().await + }) + } +} + +async fn start_relayer() { + let mut interval = tokio::time::interval(Duration::from_secs(3)); + + loop { + status_info!("Relayer", "Relayer is running"); + + interval.tick().await; } } + +async fn update_headers>(mut client: Client) { + let mut interval = tokio::time::interval(Duration::from_secs(3)); + + loop { + let result = client.update(SystemTime::now()).await; + + match result { + Ok(Some(trusted_state)) => status_ok!( + client.chain().id(), + "Updated to trusted state: {} at height {}", + trusted_state.header().hash(), + trusted_state.header().height() + ), + + Ok(None) => status_info!(client.chain().id(), "Ignoring update to a previous state"), + Err(err) => status_info!(client.chain().id(), "Error when updating headers: {}", err), + } + + interval.tick().await; + } +} + +async fn create_client( + chain_config: ChainConfig, +) -> Client> { + let chain = TendermintChain::from_config(chain_config).unwrap(); + + let store = relayer::store::persistent(format!("store_{}.db", chain.id())); + let trust_options = store.get_trust_options().unwrap(); // FIXME: unwrap + + Client::new(chain, store, trust_options).await.unwrap() +} + +fn block_on(future: F) -> F::Output { + tokio::runtime::Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap() + .block_on(future) +} diff --git a/relayer/relay/Cargo.toml b/relayer/relay/Cargo.toml index 9e41e64b68..c9c99a108f 100644 --- a/relayer/relay/Cargo.toml +++ b/relayer/relay/Cargo.toml @@ -7,11 +7,9 @@ authors = [ "Romain Ruetschi " ] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] relayer-modules = { path = "../../modules" } -tendermint = { git = "https://github.com/interchainio/tendermint-rs.git" } +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git" } anomaly = "0.2.0" humantime-serde = "1.0.0" @@ -19,3 +17,9 @@ serde = "1.0.97" serde_derive = "1.0" thiserror = "1.0.11" toml = "0.5" +async-trait = "0.1.24" +sled = "0.31.0" +serde_cbor = "0.11.1" + +[dev-dependencies] +tokio = { version = "0.2.13", features = ["macros"] } diff --git a/relayer/relay/src/chain.rs b/relayer/relay/src/chain.rs index 95d5327598..4909a0791a 100644 --- a/relayer/relay/src/chain.rs +++ b/relayer/relay/src/chain.rs @@ -1,15 +1,93 @@ -use ::tendermint::rpc; +use std::time::Duration; + +use anomaly::fail; +use serde::{de::DeserializeOwned, Serialize}; + +use ::tendermint::chain::Id as ChainId; +use ::tendermint::lite::types as tmlite; +use ::tendermint::lite::{self, Height, TrustThresholdFraction}; +use ::tendermint::rpc::Client as RpcClient; use relayer_modules::ics02_client::state::ConsensusState; use crate::config::ChainConfig; +use crate::error; pub mod tendermint; +/// Handy type alias for the type of validator set associated with a chain +pub type ValidatorSet = <::Commit as tmlite::Commit>::ValidatorSet; + +/// Defines a blockchain as understood by the relayer pub trait Chain { - type Type; - type ConsensusState: ConsensusState; + /// Type of headers for this chain + type Header: tmlite::Header + Serialize + DeserializeOwned; + + /// Type of commits for this chain + type Commit: tmlite::Commit + Serialize + DeserializeOwned; + + /// Type of consensus state for this chain + type ConsensusState: ConsensusState + Serialize + DeserializeOwned; + /// Type of RPC requester (wrapper around low-level RPC client) for this chain + type Requester: tmlite::Requester; + + /// Returns the chain's identifier + fn id(&self) -> &ChainId { + &self.config().id + } + + /// Returns the chain's configuration fn config(&self) -> &ChainConfig; - fn rpc_client(&self) -> &rpc::Client; // TODO: Define our own generic client interface? + + /// Get a low-level RPC client for this chain + fn rpc_client(&self) -> &RpcClient; + + /// Get a higher-level RPC requester for this chain + fn requester(&self) -> &Self::Requester; + + /// The trusting period configured for this chain + fn trusting_period(&self) -> Duration; + + /// The trust threshold configured for this chain + fn trust_threshold(&self) -> TrustThresholdFraction; +} + +/// Query the latest height the chain is at via a RPC query +pub async fn query_latest_height(chain: &impl Chain) -> Result { + let status = chain + .rpc_client() + .status() + .await + .map_err(|e| error::Kind::Rpc.context(e))?; + + if status.sync_info.catching_up { + fail!( + error::Kind::LightClient, + "node at {} running chain {} not caught up", + chain.config().rpc_addr, + chain.config().id, + ); + } + + Ok(status.sync_info.latest_block_height.into()) +} + +/// Query a header at the given height via the RPC requester +pub async fn query_header_at_height( + chain: &C, + height: Height, +) -> Result, error::Error> +where + C: Chain, +{ + use tmlite::Requester; + + let header = chain + .requester() + .signed_header(height) + .await + .map_err(|e| error::Kind::Rpc.context(e))?; + + Ok(header) } diff --git a/relayer/relay/src/chain/tendermint.rs b/relayer/relay/src/chain/tendermint.rs index 5f28974be3..4484e4833c 100644 --- a/relayer/relay/src/chain/tendermint.rs +++ b/relayer/relay/src/chain/tendermint.rs @@ -1,8 +1,13 @@ -use tendermint::rpc; +use std::time::Duration; + +use tendermint::block::signed_header::SignedHeader as TMCommit; +use tendermint::block::Header as TMHeader; +use tendermint::lite::TrustThresholdFraction; +use tendermint::rpc::Client as RpcClient; -use relayer_modules::ics02_client::client_type::Tendermint; use relayer_modules::ics07_tendermint::consensus_state::ConsensusState; +use crate::client::rpc_requester::RpcRequester; use crate::config::ChainConfig; use crate::error; @@ -10,25 +15,47 @@ use super::Chain; pub struct TendermintChain { config: ChainConfig, - rpc_client: rpc::Client, + rpc_client: RpcClient, + requester: RpcRequester, } impl TendermintChain { pub fn from_config(config: ChainConfig) -> Result { - let rpc_client = rpc::Client::new(config.rpc_addr.clone()); - Ok(Self { config, rpc_client }) + // TODO: Derive Clone on RpcClient in tendermint-rs + let requester = RpcRequester::new(RpcClient::new(config.rpc_addr.clone())); + let rpc_client = RpcClient::new(config.rpc_addr.clone()); + + Ok(Self { + config, + rpc_client, + requester, + }) } } impl Chain for TendermintChain { - type Type = Tendermint; + type Header = TMHeader; + type Commit = TMCommit; type ConsensusState = ConsensusState; + type Requester = RpcRequester; fn config(&self) -> &ChainConfig { &self.config } - fn rpc_client(&self) -> &rpc::Client { + fn rpc_client(&self) -> &RpcClient { &self.rpc_client } + + fn requester(&self) -> &Self::Requester { + &self.requester + } + + fn trusting_period(&self) -> Duration { + self.config.trusting_period + } + + fn trust_threshold(&self) -> TrustThresholdFraction { + TrustThresholdFraction::default() + } } diff --git a/relayer/relay/src/client.rs b/relayer/relay/src/client.rs new file mode 100644 index 0000000000..520e46a2ce --- /dev/null +++ b/relayer/relay/src/client.rs @@ -0,0 +1,397 @@ +use std::cmp::Ordering; +use std::time::{Duration, SystemTime}; + +use anomaly::fail; + +use tendermint::lite::types::{Commit, Header as _, Requester, ValidatorSet as _}; +use tendermint::lite::{SignedHeader, TrustThresholdFraction, TrustedState}; + +use crate::chain::{self, ValidatorSet}; +use crate::error; +use crate::store::{self, StoreHeight}; + +pub mod trust_options; +pub use trust_options::TrustOptions; + +pub mod rpc_requester; +pub use rpc_requester::RpcRequester; + +/// Defines a client from the point of view of the relayer. +/// +/// This is basically a wrapper around the facilities provided +/// by the light client implementation in the `tendermint-rs` crate, +/// where verified headers are stored in a trusted store. +pub struct Client +where + Chain: chain::Chain, + Store: store::Store, +{ + /// The chain this client is for + chain: Chain, + + /// The trusted store where to store verified headers + trusted_store: Store, + + /// The trusting period configured for this chain + trusting_period: Duration, + + /// The trust threshold configured for this chain + trust_threshold: TrustThresholdFraction, + + // FIXME: Use a custom type, because TrustedState states + // it holds a header at height H and validator set at height H + 1, + // whereas this trusted state holds both header and validator set at + // same height H + /// The last trusted state verified by this client + last_trusted_state: Option>, +} + +impl Client +where + Chain: chain::Chain, + Store: store::Store, +{ + /// Creates a new `Client` for the given `chain`, storing headers + /// in the given `trusted_store`, and verifying them with the + /// given `trust_options`. + /// + /// This method is async because it needs to pull the latest header + /// from the chain, verify it, and store it. + pub async fn new( + chain: Chain, + trusted_store: Store, + trust_options: TrustOptions, + ) -> Result { + let mut client = Self::new_from_trusted_store(chain, trusted_store, &trust_options)?; + + // If we managed to pull and verify a header from the chain already + if let Some(ref trusted_state) = client.last_trusted_state { + // Check that this header can be trusted with the given trust options + client + .check_trusted_header(trusted_state.last_header(), &trust_options) + .await?; + + // If the last header we trust is below the given trusted height, we need + // to fetch and verify the header at the given trusted height instead. + if trusted_state.last_header().header().height() < trust_options.height { + client.init_with_trust_options(trust_options).await?; + } + } else { + // Otherwise, init the client with the given trusted height, etc. + client.init_with_trust_options(trust_options).await?; + } + + // Perform an update already, to make sure the client is up-to-date. + // TODO: Should we leave this up to the responsibility of the caller? + let _ = client.update(SystemTime::now()).await?; + + Ok(client) + } + + /// The last state (if any) trusted by this client + pub fn last_trusted_state(&self) -> Option<&TrustedState> { + self.last_trusted_state.as_ref() + } + + /// The chain for which this client is configured + pub fn chain(&self) -> &Chain { + &self.chain + } + + /// Fetch and verify the latest header from the chain + /// + /// If the fetched header is higher than the previous trusted state, + /// and it verifies then we succeed and return it wrapped in `Some(_)`. + /// + /// If it is higher but does not verify we fail with an error. + /// + /// If it is lower we succeed but return `None`. + /// + /// If there is no trusted state yet we fail with an error. + pub async fn update( + &mut self, + now: SystemTime, + ) -> Result>, error::Error> { + match self.last_trusted_state { + Some(ref last_trusted_state) => { + let last_trusted_height = last_trusted_state.last_header().header().height(); + + let latest_header = self + .chain + .requester() + .signed_header(0) + .await + .map_err(|e| error::Kind::LightClient.context(e))?; + + let latest_validator_set = self + .chain + .requester() + .validator_set(latest_header.header().height()) + .await + .map_err(|e| error::Kind::LightClient.context(e))?; + + if latest_header.header().height() > last_trusted_height { + self.verify_header(&latest_header, &latest_validator_set, now) + .await?; + + Ok(Some(latest_header)) + } else { + Ok(None) + } + } + None => fail!(error::Kind::LightClient, "can't get last trusted state"), + } + } + + /// Verify the given signed header and validator set against the + /// trusted store. + /// + /// If the given header is already in the store, but it does not match + /// the stored hash, we fail with an error. + /// + /// Otherwise, and only if we already have a trusted state, + /// we then pull the next validator set (w.r.t to the given header) + /// and attempt to verify the new header against it. + /// If that succeeds we update the trusted store and our last trusted state. + /// + /// If there is no current trusted state, we fail with an error. + async fn verify_header( + &mut self, + new_header: &SignedHeader, + new_validator_set: &ValidatorSet, + now: SystemTime, + ) -> Result<(), error::Error> { + let in_store = self + .trusted_store + .get(StoreHeight::Given(new_header.header().height())); + + // If the given header height is already in the store + if let Ok(state) = in_store { + let stored_header = state.last_header().header(); + + // ... but it does not match the stored hash, then we fail + if stored_header.hash() != new_header.header().hash() { + fail!( + error::Kind::LightClient, + "existing trusted header {} does not match new header {}", + stored_header.hash(), + new_header.header().hash() + ) + } + } + + // If we already have a trusted state, we then pull the next validator set of + // the header we were given to verify, and attempt to verify the new + // header against it. + if let Some(ref last_trusted_state) = self.last_trusted_state { + let next_height = new_header + .header() + .height() + .checked_add(1) + .expect("height overflow"); + + let new_next_validator_set = self + .chain + .requester() + .validator_set(next_height) + .await + .map_err(|e| error::Kind::LightClient.context(e))?; + + tendermint::lite::verifier::verify_single( + last_trusted_state.clone(), + &new_header, + &new_validator_set, + &new_next_validator_set, + self.trust_threshold, + self.trusting_period, + now, + ) + .map_err(|e| error::Kind::LightClient.context(e))?; + + // TODO: Compare new header with witnesses (?) + + let new_trusted_state = + TrustedState::new(new_header.clone(), new_validator_set.clone()); + + self.update_trusted_state(new_trusted_state)?; + + Ok(()) + } else { + fail!( + error::Kind::LightClient, + "no current trusted state to verify new header with" + ) + } + } + + /// Create a new client with the given trusted store and trust options, + /// and try to restore the last trusted state from the trusted store. + fn new_from_trusted_store( + chain: Chain, + trusted_store: Store, + trust_options: &TrustOptions, + ) -> Result { + let mut client = Self { + chain, + trusted_store, + trusting_period: trust_options.trusting_period, + trust_threshold: trust_options.trust_threshold, + last_trusted_state: None, + }; + + client.restore_trusted_state()?; + + Ok(client) + } + + /// Restore the last trusted state from the state, by asking for + /// its last stored height, without any verification. + fn restore_trusted_state(&mut self) -> Result<(), error::Error> { + if let Some(last_height) = self.trusted_store.last_height() { + let last_trusted_state = self + .trusted_store + .get(store::StoreHeight::Given(last_height))?; + + self.last_trusted_state = Some(last_trusted_state); + } + + Ok(()) + } + + /// Check that the given trusted state corresponding to the given + /// trust options is valid. + /// + /// TODO: Improve doc + /// TODO: Impement rollback + /// TODO: Implement cleanup + async fn check_trusted_header( + &self, + trusted_header: &SignedHeader, + trust_options: &TrustOptions, + ) -> Result<(), error::Error> { + let primary_hash = match trust_options.height.cmp(&trusted_header.header().height()) { + Ordering::Greater => { + // TODO: Fetch from primary (?) + self.chain + .requester() + .signed_header(trust_options.height) + .await + .map_err(|e| error::Kind::Rpc.context(e))? + .header() + .hash() + } + Ordering::Equal => trust_options.hash, + Ordering::Less => { + // TODO: Implement rollback + trust_options.hash + } + }; + + if primary_hash != trusted_header.header().hash() { + // TODO: Implement cleanup + } + + Ok(()) + } + + /// Init this client with the given trust options. + /// + /// This pulls the header and validator set at the height specified in + /// the trust options, and checks their hashes against the hashes + /// specified in the trust options. + /// + /// Then validate the commit against its validator set. + /// + /// Then verify that +1/3 of the validator set signed the commit. + /// + /// If everything succeeds, add the header to the trusted store and + /// update the last trusted state with it. + async fn init_with_trust_options( + &mut self, + trust_options: TrustOptions, + ) -> Result<(), error::Error> { + // TODO: Fetch from primary (?) + let signed_header = self + .chain + .requester() + .signed_header(trust_options.height) + .await + .map_err(|e| error::Kind::Rpc.context(e))?; + + // TODO: Validate basic + + if trust_options.hash != signed_header.header().hash() { + fail!( + error::Kind::LightClient, + "expected header's hash {}, but got {}", + trust_options.hash, + signed_header.header().hash() + ) + } + + // TODO: Compare header with witnesses (?) + + // TODO: Fetch from primary (?) + let validator_set = self + .chain + .requester() + .validator_set(trust_options.height) + .await + .map_err(|e| error::Kind::Rpc.context(e))?; + + if signed_header.header().validators_hash() != validator_set.hash() { + fail!( + error::Kind::LightClient, + "expected header's validators ({}) to match those that were supplied ({})", + signed_header.header().validators_hash(), + validator_set.hash() + ) + } + + // FIXME: Is this necessary? + signed_header + .commit() + .validate(&validator_set) + .map_err(|e| error::Kind::LightClient.context(e))?; + + tendermint::lite::verifier::verify_commit_trusting( + &validator_set, + signed_header.commit(), + trust_options.trust_threshold, + ) + .map_err(|e| error::Kind::LightClient.context(e))?; + + let trusted_state = TrustedState::new(signed_header, validator_set); + self.update_trusted_state(trusted_state)?; + + Ok(()) + } + + /// Update the last trusted state with the given state, + /// and add it to the trusted store. + /// + /// This method only verifies that the validators hashes match. + /// + /// TODO: Pruning + fn update_trusted_state( + &mut self, + state: TrustedState, + ) -> Result<(), error::Error> { + if state.last_header().header().validators_hash() != state.validators().hash() { + fail!( + error::Kind::LightClient, + "expected validator's hash {}, but got {}", + state.last_header().header().validators_hash(), + state.validators().hash() + ) + } + + self.trusted_store.add(state.clone())?; + + // TODO: Pruning + + self.last_trusted_state = Some(state); + + Ok(()) + } +} diff --git a/relayer/relay/src/client/rpc_requester.rs b/relayer/relay/src/client/rpc_requester.rs new file mode 100644 index 0000000000..8ffb1cf64d --- /dev/null +++ b/relayer/relay/src/client/rpc_requester.rs @@ -0,0 +1,73 @@ +use async_trait::async_trait; + +use tendermint::block::signed_header::SignedHeader as TMCommit; +use tendermint::block::Header as TMHeader; +use tendermint::lite::{error, Height, SignedHeader}; +use tendermint::rpc; +use tendermint::validator; +use tendermint::validator::Set; +use tendermint::{block, lite}; + +/// RpcRequester wraps the Tendermint rpc::Client to provide +/// a slightly higher-level API to fetch signed headers +/// and validator sets from the chain. +pub struct RpcRequester { + client: rpc::Client, +} + +impl RpcRequester { + pub fn new(client: rpc::Client) -> Self { + Self { client } + } +} + +type TMSignedHeader = SignedHeader; + +#[async_trait] +impl lite::types::Requester for RpcRequester { + /// Request the signed header at height h. + /// If h==0, request the latest signed header. + /// TODO: use an enum instead of h==0. + async fn signed_header(&self, h: Height) -> Result { + let height: block::Height = h.into(); + let r = match height.value() { + 0 => self.client.latest_commit().await, + _ => self.client.commit(height).await, + }; + match r { + Ok(response) => Ok(response.signed_header.into()), + Err(error) => Err(error::Kind::RequestFailed.context(error).into()), + } + } + + /// Request the validator set at height h. + async fn validator_set(&self, h: Height) -> Result { + let r = self.client.validators(h).await; + match r { + Ok(response) => Ok(validator::Set::new(response.validators)), + Err(error) => Err(error::Kind::RequestFailed.context(error).into()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tendermint::lite::types::Header as LiteHeader; + use tendermint::lite::types::Requester as LiteRequester; + use tendermint::lite::types::ValidatorSet as LiteValSet; + use tendermint::rpc; + + // TODO: integration test + #[ignore] + #[tokio::test] + async fn test_val_set() { + let client = rpc::Client::new("localhost:26657".parse().unwrap()); + let req = RpcRequester::new(client); + + let r1 = req.validator_set(5).await.unwrap(); + let r2 = req.signed_header(5).await.unwrap(); + + assert_eq!(r1.hash(), r2.header().validators_hash()); + } +} diff --git a/relayer/relay/src/client/trust_options.rs b/relayer/relay/src/client/trust_options.rs new file mode 100644 index 0000000000..3f1240318b --- /dev/null +++ b/relayer/relay/src/client/trust_options.rs @@ -0,0 +1,41 @@ +use std::time::Duration; + +use anomaly::fail; +use serde_derive::{Deserialize, Serialize}; + +use tendermint::lite::{Height, TrustThresholdFraction}; +use tendermint::Hash; + +use crate::error; + +/// The trust options for a `Client` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrustOptions { + pub hash: Hash, + pub height: Height, + pub trusting_period: Duration, + pub trust_threshold: TrustThresholdFraction, +} + +impl TrustOptions { + pub fn new( + hash: Hash, + height: Height, + trusting_period: Duration, + trust_threshold: TrustThresholdFraction, + ) -> Result { + if trusting_period <= Duration::new(0, 0) { + fail!( + error::Kind::LightClient, + "trusting period must be greater than zero" + ) + } + + Ok(Self { + hash, + height, + trusting_period, + trust_threshold, + }) + } +} diff --git a/relayer/relay/src/config.rs b/relayer/relay/src/config.rs index 71edb56c83..2f513bcf9e 100644 --- a/relayer/relay/src/config.rs +++ b/relayer/relay/src/config.rs @@ -10,6 +10,27 @@ use tendermint::net; use crate::error; +/// Defaults for various fields +mod default { + use super::*; + + pub fn timeout() -> Duration { + Duration::from_secs(10) + } + + pub fn gas() -> u64 { + 200_000 + } + + pub fn rpc_addr() -> net::Address { + "localhost:26657".parse().unwrap() + } + + pub fn trusting_period() -> Duration { + Duration::from_secs(336 * 60 * 60) // 336 hours + } +} + #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct Config { pub global: GlobalConfig, @@ -17,10 +38,6 @@ pub struct Config { pub connections: Option>, // use all for default } -fn default_timeout() -> Duration { - Duration::from_secs(10) -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub enum Strategy { #[serde(rename = "naive")] @@ -35,7 +52,7 @@ impl Default for Strategy { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct GlobalConfig { - #[serde(default = "default_timeout", with = "humantime_serde")] + #[serde(default = "default::timeout", with = "humantime_serde")] pub timeout: Duration, #[serde(default)] pub strategy: Strategy, @@ -44,30 +61,24 @@ pub struct GlobalConfig { impl Default for GlobalConfig { fn default() -> Self { Self { - timeout: default_timeout(), + timeout: default::timeout(), strategy: Strategy::default(), } } } -fn default_gas() -> u64 { - 200_000 -} - -fn default_rpc_addr() -> net::Address { - "localhost:26657".parse().unwrap() -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ChainConfig { pub id: ChainId, - #[serde(default = "default_rpc_addr")] + #[serde(default = "default::rpc_addr")] pub rpc_addr: net::Address, pub account_prefix: String, pub key_name: String, pub client_ids: Vec, - #[serde(default = "default_gas")] + #[serde(default = "default::gas")] pub gas: u64, + #[serde(default = "default::trusting_period", with = "humantime_serde")] + pub trusting_period: Duration, } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/relayer/relay/src/error.rs b/relayer/relay/src/error.rs index 42c045d0b3..0ce2b1498b 100644 --- a/relayer/relay/src/error.rs +++ b/relayer/relay/src/error.rs @@ -17,11 +17,28 @@ pub enum Kind { #[error("invalid configuration")] Config, + /// RPC error (typcally raised by the RPC client or the RPC requester) #[error("RPC error")] Rpc, + + /// Light client error, typically raised by a `Client` + #[error("light client error")] + LightClient, + + /// Trusted store error, raised by instances of `Store` + #[error("store error")] + Store, } impl Kind { + /// Add a given source error as context for this error kind + /// + /// This is typically use with `map_err` as follows: + /// + /// ```ignore + /// let x = self.something.do_stuff() + /// .map_err(|e| error::Kind::Config.context(e))?; + /// ``` pub fn context(self, source: impl Into) -> Context { Context::new(self, Some(source.into())) } diff --git a/relayer/relay/src/lib.rs b/relayer/relay/src/lib.rs index fdf18567e8..fb59910267 100644 --- a/relayer/relay/src/lib.rs +++ b/relayer/relay/src/lib.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] #![deny( - warnings, + // warnings, // missing_docs, trivial_casts, trivial_numeric_casts, @@ -12,6 +12,8 @@ //! IBC Relayer implementation pub mod chain; +pub mod client; pub mod config; pub mod error; pub mod query; +pub mod store; diff --git a/relayer/relay/src/query.rs b/relayer/relay/src/query.rs index 14a930ad34..e9cdf62057 100644 --- a/relayer/relay/src/query.rs +++ b/relayer/relay/src/query.rs @@ -8,10 +8,20 @@ use crate::error; pub mod client_consensus_state; +/// The type of IBC response sent back for a given IBC `Query`. pub trait IbcResponse: Sized { + /// The type of the raw response returned by the interface used to query the chain + /// + /// TODO: Uncomment once we abstract over the IBC client + // type RawType; + + /// Build a response of this type from the initial `query` and the IBC `response`. + /// + /// TODO: Replace `AbciQuery` with `Self::RawType` fn from_abci_response(query: Query, response: AbciQuery) -> Result; } +/// Defines an IBC query pub trait IbcQuery: Sized { type Response: IbcResponse; @@ -20,6 +30,9 @@ pub trait IbcQuery: Sized { fn prove(&self) -> bool; fn data(&self) -> Vec; + /// Build a `Response` from a raw `AbciQuery` response + /// + /// TODO: Replace `AbciQuery` with `>::RawType` fn build_response(self, response: AbciQuery) -> Result { Self::Response::from_abci_response(self, response) } diff --git a/relayer/relay/src/store.rs b/relayer/relay/src/store.rs new file mode 100644 index 0000000000..7583dc5135 --- /dev/null +++ b/relayer/relay/src/store.rs @@ -0,0 +1,72 @@ +use std::path::Path; + +use serde::{de::DeserializeOwned, Serialize}; + +use tendermint::lite::types as tmlite; +use tendermint::lite::{Height, TrustedState}; + +use crate::chain::Chain; +use crate::client::trust_options::TrustOptions; +use crate::error; + +/// In-memory store +pub mod mem; + +/// Persistent store via the `sled` database +pub mod sled; + +/// Either the last stored height or a given one +pub enum StoreHeight { + /// The last stored height + Last, + + /// The given height + Given(Height), +} + +/// Defines a trusted store, which tracks: +/// +/// - the latest height a light client as synced up to +/// - all the trusted state (header+commit) the light client has verified +/// - the trust options configured for the associated chain +pub trait Store +where + C: Chain, +{ + /// Get the last height to which the light client has synced up to, if any + fn last_height(&self) -> Option; + + /// Add a trusted state to the store + fn add(&mut self, state: TrustedState) -> Result<(), error::Error>; + + /// Fetch the trusted state at the given height from the store, if it exists + fn get(&self, height: StoreHeight) -> Result, error::Error>; + + /// Check whether the trusted store contains a trusted state at the given height + fn has(&self, height: StoreHeight) -> bool { + self.get(height).is_ok() + } + + /// Get the trust options configured for the associated chain, if any + fn get_trust_options(&self) -> Result; + + /// Set the trust options for the associated chain + fn set_trust_options(&mut self, trust_options: TrustOptions) -> Result<(), error::Error>; +} + +/// Returns a persistent trusted store backed by an on-disk `sled` database +/// stored in sthe folder specified in the `path` argument. +/// +/// TODO: Remove this hideous `where` clause, once we enforce in +/// tendermint-rs that validator sets must be serializable. +pub fn persistent(db_path: impl AsRef) -> sled::SledStore +where + <::Commit as tmlite::Commit>::ValidatorSet: Serialize + DeserializeOwned, +{ + sled::SledStore::new(db_path) +} + +/// Returns a transient in-memory store +pub fn in_memory() -> mem::MemStore { + mem::MemStore::new() +} diff --git a/relayer/relay/src/store/mem.rs b/relayer/relay/src/store/mem.rs new file mode 100644 index 0000000000..0d98019299 --- /dev/null +++ b/relayer/relay/src/store/mem.rs @@ -0,0 +1,80 @@ +use std::collections::HashMap; + +use anomaly::fail; +use tendermint::lite::{types::Header, Height, TrustedState}; + +use super::{Store, StoreHeight}; + +use crate::chain::Chain; +use crate::client::trust_options::TrustOptions; +use crate::error; + +/// Transient in-memory store +pub struct MemStore { + last_height: Height, + trust_options: Option, + store: HashMap>, +} + +impl MemStore { + pub fn new() -> Self { + Self { + last_height: 0, + trust_options: None, + store: Default::default(), + } + } +} + +impl Default for MemStore { + fn default() -> Self { + Self::new() + } +} + +impl Store for MemStore { + fn last_height(&self) -> Option { + if self.last_height == 0 { + None + } else { + Some(self.last_height) + } + } + + fn add(&mut self, state: TrustedState) -> Result<(), error::Error> { + let height = state.last_header().header().height(); + + self.last_height = height; + self.store.insert(height, state); + + Ok(()) + } + + fn get(&self, height: StoreHeight) -> Result, error::Error> { + let height = match height { + StoreHeight::Last => self.last_height, + StoreHeight::Given(height) => height, + }; + + match self.store.get(&height) { + Some(state) => Ok(state.clone()), + None => fail!( + error::Kind::Store, + "could not load height {} from store", + height + ), + } + } + + fn get_trust_options(&self) -> Result { + match self.trust_options { + Some(ref trust_options) => Ok(trust_options.clone()), + None => fail!(error::Kind::Store, "no trust options in trusted store"), + } + } + + fn set_trust_options(&mut self, trust_options: TrustOptions) -> Result<(), error::Error> { + self.trust_options = Some(trust_options); + Ok(()) + } +} diff --git a/relayer/relay/src/store/sled.rs b/relayer/relay/src/store/sled.rs new file mode 100644 index 0000000000..f5dde5fd40 --- /dev/null +++ b/relayer/relay/src/store/sled.rs @@ -0,0 +1,138 @@ +use std::marker::PhantomData; +use std::path::Path; + +use anomaly::fail; +use serde::{de::DeserializeOwned, Serialize}; + +use tendermint::lite::types::{self as tmlite, Header as _}; +use tendermint::lite::TrustedState; + +use crate::chain::Chain; +use crate::client::trust_options::TrustOptions; +use crate::error; + +use super::{Store, StoreHeight}; + +/// Persistent store backed by an on-disk `sled` database. +/// +/// TODO: Remove this hideous `where` clause, once we enforce in +/// tendermint-rs that validator sets must be serializable. +pub struct SledStore +where + <::Commit as tmlite::Commit>::ValidatorSet: Serialize + DeserializeOwned, +{ + db: sled::Db, + marker: PhantomData, +} + +impl SledStore +where + <::Commit as tmlite::Commit>::ValidatorSet: Serialize + DeserializeOwned, +{ + pub fn new(path: impl AsRef) -> Self { + Self { + db: sled::open(path).unwrap(), // FIXME: Unwrap + marker: PhantomData, + } + } +} + +impl Store for SledStore +where + <::Commit as tmlite::Commit>::ValidatorSet: Serialize + DeserializeOwned, +{ + fn last_height(&self) -> Option { + let bytes = self + .db + .get(b"last_height") + .map_err(|e| error::Kind::Store.context(e)) + .unwrap(); // FIXME + + match bytes { + Some(bytes) => { + let last_height = serde_cbor::from_slice(&bytes) + .map_err(|e| error::Kind::Store.context(e)) + .unwrap(); // FIXME + + Some(last_height) + } + None => None, + } + } + + fn add( + &mut self, + trusted_state: TrustedState, + ) -> Result<(), error::Error> { + let bytes = + serde_cbor::to_vec(&trusted_state).map_err(|e| error::Kind::Store.context(e))?; + + let key = format!( + "trusted_state/{}", + trusted_state.last_header().header().height() + ); + + self.db + .insert(key.as_bytes(), bytes) + .map(|_| ()) + .map_err(|e| error::Kind::Store.context(e))?; + + Ok(()) + } + + fn get(&self, height: StoreHeight) -> Result, error::Error> { + let height = match height { + StoreHeight::Last => self.last_height().unwrap_or(0), + StoreHeight::Given(height) => height, + }; + + let key = format!("trusted_state/{}", height); + let bytes = self + .db + .get(&key) + .map_err(|e| error::Kind::Store.context(e))?; + + match bytes { + Some(bytes) => { + let trusted_state = + serde_cbor::from_slice(&bytes).map_err(|e| error::Kind::Store.context(e))?; + + Ok(trusted_state) + } + None => fail!( + error::Kind::Store, + "could not load height {} from store", + height + ), + } + } + + fn get_trust_options(&self) -> Result { + let bytes = self + .db + .get(b"trust_options") // TODO: Extract as constant + .map_err(|e| error::Kind::Store.context(e))?; + + match bytes { + Some(bytes) => { + let trust_options = + serde_cbor::from_slice(&bytes).map_err(|e| error::Kind::Store.context(e))?; + + Ok(trust_options) + } + None => fail!(error::Kind::Store, "no trust options in trusted store"), + } + } + + fn set_trust_options(&mut self, trust_options: TrustOptions) -> Result<(), error::Error> { + let bytes = + serde_cbor::to_vec(&trust_options).map_err(|e| error::Kind::Store.context(e))?; + + self.db + .insert(b"trust_options", bytes) + .map(|_| ()) + .map_err(|e| error::Kind::Store.context(e))?; + + Ok(()) + } +} diff --git a/relayer/relay/tests/config/fixtures/relayer_conf_example.toml b/relayer/relay/tests/config/fixtures/relayer_conf_example.toml index 9a21d46282..f8a1bdb33f 100644 --- a/relayer/relay/tests/config/fixtures/relayer_conf_example.toml +++ b/relayer/relay/tests/config/fixtures/relayer_conf_example.toml @@ -18,8 +18,9 @@ strategy = "naive" trusting_period = "336h" [[chains]] - id = "chain-B" - rpc_addr = "localhost:26557" + id = "chain_B" + # rpc_addr = "localhost:26557" + rpc_addr = "localhost:26657" account_prefix = "cosmos" key_name = "testkey" client_ids = ["clB1"] From 7f00e55bfcde2f34218ab3e620bc51f124f30d58 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 24 Mar 2020 15:33:26 +0100 Subject: [PATCH 08/63] Add type-safe wrapper around `sled` databases (#41) * Add type-safe wrapper around sled database * Cleanup now redundant bounds in store * Make SledStore::new safe --- relayer/cli/src/commands/light/init.rs | 2 +- relayer/cli/src/commands/start.rs | 2 +- relayer/relay/src/lib.rs | 1 + relayer/relay/src/store.rs | 12 +-- relayer/relay/src/store/sled.rs | 121 ++++++++----------------- relayer/relay/src/util.rs | 1 + relayer/relay/src/util/sled.rs | 98 ++++++++++++++++++++ 7 files changed, 143 insertions(+), 94 deletions(-) create mode 100644 relayer/relay/src/util.rs create mode 100644 relayer/relay/src/util/sled.rs diff --git a/relayer/cli/src/commands/light/init.rs b/relayer/cli/src/commands/light/init.rs index c099395ba9..5a73fc9b91 100644 --- a/relayer/cli/src/commands/light/init.rs +++ b/relayer/cli/src/commands/light/init.rs @@ -94,7 +94,7 @@ impl Runnable for InitCmd { .unwrap(); let mut store: SledStore = - relayer::store::persistent(format!("store_{}.db", chain_config.id)); + relayer::store::persistent(format!("store_{}.db", chain_config.id)).unwrap(); // FIXME: unwrap store.set_trust_options(trust_options).unwrap(); // FIXME: unwrap diff --git a/relayer/cli/src/commands/start.rs b/relayer/cli/src/commands/start.rs index ff21ebdeee..a4bc419b36 100644 --- a/relayer/cli/src/commands/start.rs +++ b/relayer/cli/src/commands/start.rs @@ -89,7 +89,7 @@ async fn create_client( ) -> Client> { let chain = TendermintChain::from_config(chain_config).unwrap(); - let store = relayer::store::persistent(format!("store_{}.db", chain.id())); + let store = relayer::store::persistent(format!("store_{}.db", chain.id())).unwrap(); //FIXME: unwrap let trust_options = store.get_trust_options().unwrap(); // FIXME: unwrap Client::new(chain, store, trust_options).await.unwrap() diff --git a/relayer/relay/src/lib.rs b/relayer/relay/src/lib.rs index fb59910267..0e9cf139fb 100644 --- a/relayer/relay/src/lib.rs +++ b/relayer/relay/src/lib.rs @@ -17,3 +17,4 @@ pub mod config; pub mod error; pub mod query; pub mod store; +pub mod util; diff --git a/relayer/relay/src/store.rs b/relayer/relay/src/store.rs index 7583dc5135..c24f96afe5 100644 --- a/relayer/relay/src/store.rs +++ b/relayer/relay/src/store.rs @@ -1,8 +1,5 @@ use std::path::Path; -use serde::{de::DeserializeOwned, Serialize}; - -use tendermint::lite::types as tmlite; use tendermint::lite::{Height, TrustedState}; use crate::chain::Chain; @@ -16,6 +13,7 @@ pub mod mem; pub mod sled; /// Either the last stored height or a given one +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum StoreHeight { /// The last stored height Last, @@ -56,13 +54,7 @@ where /// Returns a persistent trusted store backed by an on-disk `sled` database /// stored in sthe folder specified in the `path` argument. -/// -/// TODO: Remove this hideous `where` clause, once we enforce in -/// tendermint-rs that validator sets must be serializable. -pub fn persistent(db_path: impl AsRef) -> sled::SledStore -where - <::Commit as tmlite::Commit>::ValidatorSet: Serialize + DeserializeOwned, -{ +pub fn persistent(db_path: impl AsRef) -> Result, error::Error> { sled::SledStore::new(db_path) } diff --git a/relayer/relay/src/store/sled.rs b/relayer/relay/src/store/sled.rs index f5dde5fd40..f7dcf74767 100644 --- a/relayer/relay/src/store/sled.rs +++ b/relayer/relay/src/store/sled.rs @@ -1,15 +1,14 @@ -use std::marker::PhantomData; use std::path::Path; use anomaly::fail; -use serde::{de::DeserializeOwned, Serialize}; -use tendermint::lite::types::{self as tmlite, Header as _}; -use tendermint::lite::TrustedState; +use tendermint::lite::types::Header as _; +use tendermint::lite::{Height, TrustedState}; use crate::chain::Chain; use crate::client::trust_options::TrustOptions; use crate::error; +use crate::util::sled::{self as sled_util, KeyValueDb, SingleDb}; use super::{Store, StoreHeight}; @@ -17,65 +16,48 @@ use super::{Store, StoreHeight}; /// /// TODO: Remove this hideous `where` clause, once we enforce in /// tendermint-rs that validator sets must be serializable. -pub struct SledStore -where - <::Commit as tmlite::Commit>::ValidatorSet: Serialize + DeserializeOwned, -{ +#[derive(Debug)] +pub struct SledStore { db: sled::Db, - marker: PhantomData, + last_height_db: SingleDb, + trust_options_db: SingleDb, + trusted_state_db: KeyValueDb>, } -impl SledStore -where - <::Commit as tmlite::Commit>::ValidatorSet: Serialize + DeserializeOwned, -{ - pub fn new(path: impl AsRef) -> Self { - Self { - db: sled::open(path).unwrap(), // FIXME: Unwrap - marker: PhantomData, - } +impl SledStore { + pub fn new(path: impl AsRef) -> Result { + let db = sled::open(path).map_err(|e| error::Kind::Store.context(e))?; + + Ok(Self { + db, + last_height_db: sled_util::single("last_height/"), + trust_options_db: sled_util::single("trust_options/"), + trusted_state_db: sled_util::key_value("trusted_state/"), + }) } } -impl Store for SledStore -where - <::Commit as tmlite::Commit>::ValidatorSet: Serialize + DeserializeOwned, -{ - fn last_height(&self) -> Option { - let bytes = self - .db - .get(b"last_height") - .map_err(|e| error::Kind::Store.context(e)) - .unwrap(); // FIXME - - match bytes { - Some(bytes) => { - let last_height = serde_cbor::from_slice(&bytes) - .map_err(|e| error::Kind::Store.context(e)) - .unwrap(); // FIXME - - Some(last_height) - } - None => None, - } +impl SledStore { + fn set_last_height(&self, height: Height) -> Result<(), error::Error> { + self.last_height_db.set(&self.db, &height) + } +} + +impl Store for SledStore { + fn last_height(&self) -> Option { + self.last_height_db.get(&self.db).unwrap() // FIXME } fn add( &mut self, trusted_state: TrustedState, ) -> Result<(), error::Error> { - let bytes = - serde_cbor::to_vec(&trusted_state).map_err(|e| error::Kind::Store.context(e))?; + let height = trusted_state.last_header().header().height(); - let key = format!( - "trusted_state/{}", - trusted_state.last_header().header().height() - ); + self.trusted_state_db + .insert(&self.db, &height, &trusted_state)?; - self.db - .insert(key.as_bytes(), bytes) - .map(|_| ()) - .map_err(|e| error::Kind::Store.context(e))?; + self.set_last_height(height)?; Ok(()) } @@ -86,19 +68,10 @@ where StoreHeight::Given(height) => height, }; - let key = format!("trusted_state/{}", height); - let bytes = self - .db - .get(&key) - .map_err(|e| error::Kind::Store.context(e))?; + let state = self.trusted_state_db.fetch(&self.db, &height)?; - match bytes { - Some(bytes) => { - let trusted_state = - serde_cbor::from_slice(&bytes).map_err(|e| error::Kind::Store.context(e))?; - - Ok(trusted_state) - } + match state { + Some(state) => Ok(state), None => fail!( error::Kind::Store, "could not load height {} from store", @@ -108,31 +81,15 @@ where } fn get_trust_options(&self) -> Result { - let bytes = self - .db - .get(b"trust_options") // TODO: Extract as constant - .map_err(|e| error::Kind::Store.context(e))?; - - match bytes { - Some(bytes) => { - let trust_options = - serde_cbor::from_slice(&bytes).map_err(|e| error::Kind::Store.context(e))?; - - Ok(trust_options) - } + let trust_options = self.trust_options_db.get(&self.db)?; + + match trust_options { + Some(trust_options) => Ok(trust_options), None => fail!(error::Kind::Store, "no trust options in trusted store"), } } fn set_trust_options(&mut self, trust_options: TrustOptions) -> Result<(), error::Error> { - let bytes = - serde_cbor::to_vec(&trust_options).map_err(|e| error::Kind::Store.context(e))?; - - self.db - .insert(b"trust_options", bytes) - .map(|_| ()) - .map_err(|e| error::Kind::Store.context(e))?; - - Ok(()) + self.trust_options_db.set(&self.db, &trust_options) } } diff --git a/relayer/relay/src/util.rs b/relayer/relay/src/util.rs new file mode 100644 index 0000000000..e7aaf7680e --- /dev/null +++ b/relayer/relay/src/util.rs @@ -0,0 +1 @@ +pub mod sled; diff --git a/relayer/relay/src/util/sled.rs b/relayer/relay/src/util/sled.rs new file mode 100644 index 0000000000..761d309aa5 --- /dev/null +++ b/relayer/relay/src/util/sled.rs @@ -0,0 +1,98 @@ +use serde::{de::DeserializeOwned, Serialize}; +use std::marker::PhantomData; + +use crate::error; + +pub fn single(prefix: impl Into>) -> SingleDb { + SingleDb::new(prefix) +} + +pub fn key_value(prefix: impl Into>) -> KeyValueDb { + KeyValueDb::new(prefix) +} + +pub type SingleDb = KeyValueDb<(), V>; + +impl SingleDb +where + V: Serialize + DeserializeOwned, +{ + pub fn get(&self, db: &sled::Db) -> Result, error::Error> { + self.fetch(&db, &()) + } + + pub fn set(&self, db: &sled::Db, value: &V) -> Result<(), error::Error> { + self.insert(&db, &(), &value) + } +} + +#[derive(Clone, Debug)] +pub struct KeyValueDb { + prefix: Vec, + marker: PhantomData<(K, V)>, +} + +impl KeyValueDb { + pub fn new(prefix: impl Into>) -> Self { + Self { + prefix: prefix.into(), + marker: PhantomData, + } + } +} + +impl KeyValueDb +where + K: Serialize, + V: Serialize + DeserializeOwned, +{ + fn prefixed_key(&self, mut key_bytes: Vec) -> Vec { + let mut prefix_bytes = self.prefix.clone(); + prefix_bytes.append(&mut key_bytes); + prefix_bytes + } + + pub fn fetch(&self, db: &sled::Db, key: &K) -> Result, error::Error> { + let key_bytes = serde_cbor::to_vec(&key).map_err(|e| error::Kind::Store.context(e))?; + + let prefixed_key_bytes = self.prefixed_key(key_bytes); + + let value_bytes = db + .get(prefixed_key_bytes) + .map_err(|e| error::Kind::Store.context(e))?; + + match value_bytes { + Some(bytes) => { + let value = + serde_cbor::from_slice(&bytes).map_err(|e| error::Kind::Store.context(e))?; + Ok(value) + } + None => Ok(None), + } + } + + // pub fn has(&self, key: &K) -> Result, error::Error> { + // let key_bytes = serde_cbor::to_vec(&key).map_err(|e| error::Kind::Store.context(e))?; + + // let exists = self + // .db + // .exists(key_bytes) + // .map_err(|e| error::Kind::Store.context(e))?; + + // Ok(exists) + // } + + pub fn insert(&self, db: &sled::Db, key: &K, value: &V) -> Result<(), error::Error> { + let key_bytes = serde_cbor::to_vec(&key).map_err(|e| error::Kind::Store.context(e))?; + + let prefixed_key_bytes = self.prefixed_key(key_bytes); + + let value_bytes = serde_cbor::to_vec(&value).map_err(|e| error::Kind::Store.context(e))?; + + db.insert(prefixed_key_bytes, value_bytes) + .map(|_| ()) + .map_err(|e| error::Kind::Store.context(e))?; + + Ok(()) + } +} From 18a98e1eb667a66a1529e7e8e6319dc6ed6bd9df Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 24 Mar 2020 15:49:32 +0100 Subject: [PATCH 09/63] Add structured logging to the light client (#40) * Add structured logging to the light client * Mention verbose flag in README --- README.md | 26 ++++--- relayer/cli/Cargo.toml | 2 + relayer/cli/src/commands/start.rs | 30 ++++---- relayer/relay/Cargo.toml | 7 +- relayer/relay/src/client.rs | 117 +++++++++++++++++++++++++++++- relayer/relay/src/store.rs | 2 +- relayer/relay/src/store/mem.rs | 8 +- relayer/relay/src/store/sled.rs | 6 +- 8 files changed, 159 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 542455429f..0be345b1e0 100644 --- a/README.md +++ b/README.md @@ -13,30 +13,32 @@ THIS PROJECT IS UNDER HEAVY DEVELOPMENT AND IS NOT IN A WORKING STAGE NOW, USE A ## Usage Provided that one has a Tendermint node running on port 26657, one can -configure and spawn two light clients for this chain with the following commands: +configure and spawn two light clients for this chain with the following commands (to be run in the `ibc-rs` directory): 1. Fetch a trusted header from the chain: -```bash -$ curl -s http://localhost:26657/status | jq '.result.sync_info|.latest_block_hash,.latest_block_height' -``` + ```bash + $ curl -s http://localhost:26657/status | jq '.result.sync_info|.latest_block_hash,.latest_block_height' + ``` 2. Initialize a light client for chain A with the trusted height and hash fetched in step 1: -```bash -ibc-rs > cargo run --bin relayer -- -c ./relayer/relay/tests/config/fixtures/relayer_conf_example.toml light init -x HASH -h HEIGHT chain_A -``` -> Replace `HASH` and `HEIGHT` with the appropriate values. + ```bash + ibc-rs $ cargo run --bin relayer -- -c ./relayer/relay/tests/config/fixtures/relayer_conf_example.toml light init -x HASH -h HEIGHT chain_A + ``` + > Replace `HASH` and `HEIGHT` with the appropriate values. 3. Repeat step 1 and 2 above for `chain_B`. -> For this, update the height and hash, and change the chain identifier in the command above from `chain_A` to `chain_B`. + > For this, update the height and hash, and change the chain identifier in the command above from `chain_A` to `chain_B`. 4. Start the light clients and a dummy relayer thread: -```bash -ibc-rs > cargo run --bin relayer -- -c ./relayer/relay/tests/config/fixtures/relayer_conf_example.toml run -``` + ```bash + ibc-rs $ cargo run --bin relayer -- -c ./relayer/relay/tests/config/fixtures/relayer_conf_example.toml run + ``` + +**Note:** Add a `-v` flag to the commands above to see detailed log output, eg. `cargo run --bin relayer -- -v -c ./relayer/relay/tests/config/fixtures/relayer_conf_example.toml run` ## License diff --git a/relayer/cli/Cargo.toml b/relayer/cli/Cargo.toml index d62d826cb5..2ab37e9e83 100644 --- a/relayer/cli/Cargo.toml +++ b/relayer/cli/Cargo.toml @@ -17,6 +17,8 @@ serde = { version = "1", features = ["serde_derive"] } thiserror = "1" abscissa_tokio = "0.5.1" tokio = "0.2.13" +tracing-subscriber = "0.2.3" +tracing = "0.1.13" [dependencies.abscissa_core] version = "0.5.2" diff --git a/relayer/cli/src/commands/start.rs b/relayer/cli/src/commands/start.rs index a4bc419b36..275da47160 100644 --- a/relayer/cli/src/commands/start.rs +++ b/relayer/cli/src/commands/start.rs @@ -4,6 +4,7 @@ use std::time::{Duration, SystemTime}; // use crate::application::APPLICATION; use crate::prelude::*; +use abscissa_core::tracing::{debug, error, info, warn}; use abscissa_core::{Command, Options, Runnable}; use tendermint::lite::types::Header; @@ -24,20 +25,18 @@ impl Runnable for StartCmd { // FIXME: This just hangs and never runs the given future // abscissa_tokio::run(&APPLICATION, ...).unwrap(); + debug!("launching 'start' command"); + block_on(async { for chain_config in config.chains { - status_info!( - "Relayer", - "Spawning light client for chain {}", - chain_config.id - ); + info!(chain.id = %chain_config.id, "spawning light client"); let _handle = tokio::spawn(async move { let client = create_client(chain_config).await; let trusted_state = client.last_trusted_state().unwrap(); - status_ok!( - client.chain().id(), + info!( + chain.id = %client.chain().id(), "Spawned new client now at trusted state: {} at height {}", trusted_state.last_header().header().hash(), trusted_state.last_header().header().height(), @@ -56,28 +55,33 @@ async fn start_relayer() { let mut interval = tokio::time::interval(Duration::from_secs(3)); loop { - status_info!("Relayer", "Relayer is running"); - + info!(target: "relayer_cli::relayer", "Relayer is running"); interval.tick().await; } } async fn update_headers>(mut client: Client) { + debug!(chain.id = %client.chain().id(), "updating headers"); + let mut interval = tokio::time::interval(Duration::from_secs(3)); loop { let result = client.update(SystemTime::now()).await; match result { - Ok(Some(trusted_state)) => status_ok!( - client.chain().id(), + Ok(Some(trusted_state)) => info!( + chain.id = %client.chain().id(), "Updated to trusted state: {} at height {}", trusted_state.header().hash(), trusted_state.header().height() ), - Ok(None) => status_info!(client.chain().id(), "Ignoring update to a previous state"), - Err(err) => status_info!(client.chain().id(), "Error when updating headers: {}", err), + Ok(None) => { + warn!(chain.id = %client.chain().id(), "Ignoring update to a previous state") + } + Err(err) => { + error!(chain.id = %client.chain().id(), "Error when updating headers: {}", err) + } } interval.tick().await; diff --git a/relayer/relay/Cargo.toml b/relayer/relay/Cargo.toml index c9c99a108f..bb9d1f32af 100644 --- a/relayer/relay/Cargo.toml +++ b/relayer/relay/Cargo.toml @@ -12,14 +12,15 @@ relayer-modules = { path = "../../modules" } tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git" } anomaly = "0.2.0" +async-trait = "0.1.24" humantime-serde = "1.0.0" serde = "1.0.97" +serde_cbor = "0.11.1" serde_derive = "1.0" +sled = { version = "0.31.0", features = ["no_metrics", "no_logs"] } thiserror = "1.0.11" toml = "0.5" -async-trait = "0.1.24" -sled = "0.31.0" -serde_cbor = "0.11.1" +tracing = "0.1.13" [dev-dependencies] tokio = { version = "0.2.13", features = ["macros"] } diff --git a/relayer/relay/src/client.rs b/relayer/relay/src/client.rs index 520e46a2ce..013adaf51d 100644 --- a/relayer/relay/src/client.rs +++ b/relayer/relay/src/client.rs @@ -2,6 +2,7 @@ use std::cmp::Ordering; use std::time::{Duration, SystemTime}; use anomaly::fail; +use tracing::debug; use tendermint::lite::types::{Commit, Header as _, Requester, ValidatorSet as _}; use tendermint::lite::{SignedHeader, TrustThresholdFraction, TrustedState}; @@ -66,14 +67,25 @@ where // If we managed to pull and verify a header from the chain already if let Some(ref trusted_state) = client.last_trusted_state { + debug!("found last trusted state"); + // Check that this header can be trusted with the given trust options client .check_trusted_header(trusted_state.last_header(), &trust_options) .await?; + debug!("trusted header was valid"); + // If the last header we trust is below the given trusted height, we need // to fetch and verify the header at the given trusted height instead. - if trusted_state.last_header().header().height() < trust_options.height { + let last_header_height = trusted_state.last_header().header().height(); + + if last_header_height < trust_options.height { + debug!( + last_header.height = last_header_height, + trust_options.height, "last header height below trust options height" + ); + client.init_with_trust_options(trust_options).await?; } } else { @@ -231,6 +243,11 @@ where trusted_store: Store, trust_options: &TrustOptions, ) -> Result { + debug!( + chain.id = %chain.id(), + "initializing client from trusted store", + ); + let mut client = Self { chain, trusted_store, @@ -247,12 +264,28 @@ where /// Restore the last trusted state from the state, by asking for /// its last stored height, without any verification. fn restore_trusted_state(&mut self) -> Result<(), error::Error> { - if let Some(last_height) = self.trusted_store.last_height() { + if let Some(last_height) = self.trusted_store.last_height()? { + debug!( + chain.id = %self.chain.id(), + "restoring trusted state from store", + ); + let last_trusted_state = self .trusted_store .get(store::StoreHeight::Given(last_height))?; + debug!( + chain.id = %self.chain.id(), + last_height = last_height, + "found trusted state in store", + ); + self.last_trusted_state = Some(last_trusted_state); + } else { + debug!( + chain.id = %self.chain.id(), + "no last height recorded in trusted store", + ); } Ok(()) @@ -269,8 +302,20 @@ where trusted_header: &SignedHeader, trust_options: &TrustOptions, ) -> Result<(), error::Error> { + debug!( + chain.id = %self.chain.id(), + "restoring trusted state from store", + ); + let primary_hash = match trust_options.height.cmp(&trusted_header.header().height()) { Ordering::Greater => { + debug!( + chain.id = %self.chain.id(), + trust_options.height, + trusted_header.height = trusted_header.header().height(), + "trusted options height is greater than header height in trust store", + ); + // TODO: Fetch from primary (?) self.chain .requester() @@ -280,17 +325,55 @@ where .header() .hash() } - Ordering::Equal => trust_options.hash, + Ordering::Equal => { + debug!( + chain.id = %self.chain.id(), + trust_options.height, + trusted_header.height = trusted_header.header().height(), + "trusted options height is equal to header height in trust store", + ); + + trust_options.hash + } Ordering::Less => { + debug!( + chain.id = %self.chain.id(), + trust_options.height, + trusted_header.height = trusted_header.header().height(), + "trusted options height is lesser than header height in trust store. TODO: rollback", + ); + // TODO: Implement rollback + // trust_options.hash } }; + debug!( + chain.id = %self.chain.id(), + primary.hash = %primary_hash, + trusted_header.hash = %trusted_header.header().hash(), + "comparing trusted_header.hash against primary.hash", + ); + if primary_hash != trusted_header.header().hash() { // TODO: Implement cleanup + + debug!( + chain.id = %self.chain.id(), + primary.hash = %primary_hash, + trusted_header.hash = %trusted_header.header().hash(), + "hash do not match! TODO: cleanup", + ); } + debug!( + chain.id = %self.chain.id(), + primary.hash = %primary_hash, + trusted_header.hash = %trusted_header.header().hash(), + "headers hashes match", + ); + Ok(()) } @@ -310,6 +393,8 @@ where &mut self, trust_options: TrustOptions, ) -> Result<(), error::Error> { + debug!("initialize client with given trust_options"); + // TODO: Fetch from primary (?) let signed_header = self .chain @@ -318,6 +403,12 @@ where .await .map_err(|e| error::Kind::Rpc.context(e))?; + debug!( + trust_options.height, + header.hash = %signed_header.header().hash(), + "fetched header at height {}", trust_options.height + ); + // TODO: Validate basic if trust_options.hash != signed_header.header().hash() { @@ -331,6 +422,8 @@ where // TODO: Compare header with witnesses (?) + debug!("TODO: compare header with witnesses"); + // TODO: Fetch from primary (?) let validator_set = self .chain @@ -339,6 +432,12 @@ where .await .map_err(|e| error::Kind::Rpc.context(e))?; + debug!( + trust_options.height, + validator_set.hash = %validator_set.hash(), + header.validators_hash = %signed_header.header().validators_hash(), + "fetched validator set at height {}", trust_options.height); + if signed_header.header().validators_hash() != validator_set.hash() { fail!( error::Kind::LightClient, @@ -348,12 +447,16 @@ where ) } + debug!("validator set hashes match"); + // FIXME: Is this necessary? signed_header .commit() .validate(&validator_set) .map_err(|e| error::Kind::LightClient.context(e))?; + debug!("header is valid"); + tendermint::lite::verifier::verify_commit_trusting( &validator_set, signed_header.commit(), @@ -361,6 +464,8 @@ where ) .map_err(|e| error::Kind::LightClient.context(e))?; + debug!("verify_commit_trusting result is valid"); + let trusted_state = TrustedState::new(signed_header, validator_set); self.update_trusted_state(trusted_state)?; @@ -390,6 +495,12 @@ where // TODO: Pruning + debug!( + state.header.hash = %state.last_header().header().hash(), + state.header.height = state.last_header().header().height(), + "updated trusted store" + ); + self.last_trusted_state = Some(state); Ok(()) diff --git a/relayer/relay/src/store.rs b/relayer/relay/src/store.rs index c24f96afe5..9c6996b8a7 100644 --- a/relayer/relay/src/store.rs +++ b/relayer/relay/src/store.rs @@ -32,7 +32,7 @@ where C: Chain, { /// Get the last height to which the light client has synced up to, if any - fn last_height(&self) -> Option; + fn last_height(&self) -> Result, error::Error>; /// Add a trusted state to the store fn add(&mut self, state: TrustedState) -> Result<(), error::Error>; diff --git a/relayer/relay/src/store/mem.rs b/relayer/relay/src/store/mem.rs index 0d98019299..4de7d0ebbd 100644 --- a/relayer/relay/src/store/mem.rs +++ b/relayer/relay/src/store/mem.rs @@ -33,11 +33,11 @@ impl Default for MemStore { } impl Store for MemStore { - fn last_height(&self) -> Option { + fn last_height(&self) -> Result, error::Error> { if self.last_height == 0 { - None + Ok(None) } else { - Some(self.last_height) + Ok(Some(self.last_height)) } } @@ -52,7 +52,7 @@ impl Store for MemStore { fn get(&self, height: StoreHeight) -> Result, error::Error> { let height = match height { - StoreHeight::Last => self.last_height, + StoreHeight::Last => self.last_height()?.unwrap_or(0), StoreHeight::Given(height) => height, }; diff --git a/relayer/relay/src/store/sled.rs b/relayer/relay/src/store/sled.rs index f7dcf74767..5af1ffd072 100644 --- a/relayer/relay/src/store/sled.rs +++ b/relayer/relay/src/store/sled.rs @@ -44,8 +44,8 @@ impl SledStore { } impl Store for SledStore { - fn last_height(&self) -> Option { - self.last_height_db.get(&self.db).unwrap() // FIXME + fn last_height(&self) -> Result, error::Error> { + self.last_height_db.get(&self.db) } fn add( @@ -64,7 +64,7 @@ impl Store for SledStore { fn get(&self, height: StoreHeight) -> Result, error::Error> { let height = match height { - StoreHeight::Last => self.last_height().unwrap_or(0), + StoreHeight::Last => self.last_height()?.unwrap_or(0), StoreHeight::Given(height) => height, }; From 88e0c0cc49b8ea773b445b1bb95419a1f8117ab0 Mon Sep 17 00:00:00 2001 From: istoilkovska Date: Thu, 2 Apr 2020 18:43:36 +0200 Subject: [PATCH 10/63] cleanup in relayer TLA spec --- verification/spec/relayer/environment.tla | 80 ++----- verification/spec/relayer/relayer.tla | 251 +++++++++++++++------- 2 files changed, 186 insertions(+), 145 deletions(-) diff --git a/verification/spec/relayer/environment.tla b/verification/spec/relayer/environment.tla index 2ebc89e410..4fcb5279be 100644 --- a/verification/spec/relayer/environment.tla +++ b/verification/spec/relayer/environment.tla @@ -52,6 +52,10 @@ GetLatestHeight(chainID) == \* get the client height of the client for the counterparty chain GetCounterpartyClientHeight(chainID) == chains[chainID].counterpartyClientHeight + +\* get the ID of the counterparty chain of chainID +GetCounterpartyChainID(chainID) == + IF chainID = "chainA" THEN "chainB" ELSE "chainA" \* get the client ID of the client for chainID GetClientID(chainID) == @@ -59,8 +63,16 @@ GetClientID(chainID) == \* get the client ID of the client for the counterparty of chainID GetCounterpartyClientID(chainID) == - IF chainID = "chainA" THEN "clB" ELSE "clA" + IF chainID = "chainA" THEN "clB" ELSE "clA" + +\* returns true if the counterparty client is initialized on chainID +IsCounterpartyClientOnChain(chainID) == + chains[chainID].counterpartyClientHeight /= nullHeight +\* returns true if the counterparty client height on chainID is greater or equal than height +CounterpartyClientHeightUpdated(chainID, height) == + chains[chainID].counterpartyClientHeight >= height + (*************** Light client update operators ***************) @@ -273,71 +285,9 @@ Invariants \* Type invariant TypeOK == /\ chains \in [ChainIDs -> Chains] - /\ pendingDatagrams \in [ChainIDs -> SUBSET Datagrams] - -(************ -Helper operators used in properties -************) - -\* returns true if there is a "CreateClient" datagram -\* in the pending datagrams for chainID -IsCreateClientInPendingDatagrams(chainID) == - \E h \in Heights: - [type |-> "CreateClient", clientID |-> GetCounterpartyClientID(chainID), height |-> h] - \in pendingDatagrams[chainID] - -\* returns true if there is a "ClientUpdate" datagram -\* in the pending datagrams for chainID and height h -IsClientUpdateInPendingDatagrams(chainID, h) == - [type |-> "ClientUpdate", clientID |-> GetCounterpartyClientID(chainID), height |-> h] - \in pendingDatagrams[chainID] - - -\* returns true if the counterparty client is initialized on chainID -IsCounterpartyClientOnChain(chainID) == - chains[chainID].counterpartyClientHeight /= nullHeight - -\* returns true if the counterparty client height on chainID is greater or equal than height -CounterpartyClientHeightUpdated(chainID, height) == - chains[chainID].counterpartyClientHeight >= height - -(************ -Properties -************) - -\* it ALWAYS holds that, for every chainID: -\* - if -\* * there is a "CreateClient" datagram in pending datagrams of chainID -\* * a counterparty client does not exist on chainID -\* - then -\* * the counterparty client is EVENTUALLY created on chainID -ClientCreated == - [](\A chainID \in ChainIDs : - (/\ IsCreateClientInPendingDatagrams(chainID) - /\ ~IsCounterpartyClientOnChain(chainID)) - => (<>(IsCounterpartyClientOnChain(chainID)))) - -\* it ALWAYS holds that, for every chainID: -\* - if -\* * there is a "ClientUpdate" datagram in pending datagrams of chainID -\* for height h -\* * the counterparty client height is smaller than h -\* - then -\* * the counterparty client height is EVENTUALLY udpated to at least h -ClientUpdated == - [](\A chainID \in ChainIDs : \A h \in Heights : - (/\ IsClientUpdateInPendingDatagrams(chainID, h) - /\ GetCounterpartyClientHeight(chainID) < h) - => (<>(CounterpartyClientHeightUpdated(chainID, h)))) - -\* for every chainID, it is always the case that the height of the chain -\* does not decrease -HeightsDontDecrease == - [](\A chainID \in ChainIDs : \A h \in Heights : - (chains[chainID].height = h) - => <>(chains[chainID].height >= h)) + /\ pendingDatagrams \in [ChainIDs -> SUBSET Datagrams] ============================================================================= \* Modification History -\* Last modified Wed Mar 25 17:34:23 CET 2020 by ilinastoilkovska +\* Last modified Thu Apr 02 18:27:47 CEST 2020 by ilinastoilkovska \* Created Fri Mar 13 19:48:22 CET 2020 by ilinastoilkovska diff --git a/verification/spec/relayer/relayer.tla b/verification/spec/relayer/relayer.tla index 145313c859..649ec7c70c 100644 --- a/verification/spec/relayer/relayer.tla +++ b/verification/spec/relayer/relayer.tla @@ -4,11 +4,10 @@ EXTENDS Naturals, FiniteSets CONSTANTS MaxHeight \* maximal height of all the chains in the system -VARIABLES chains, \* a function that assigns to each chainID a chain record - pendingDatagrams, \* a function that assigns to each chainID a set of pending datagrams - relayerClientHeights, \* a function that assigns to each ClientID a height +VARIABLES chains, \* a function that assigns a chain record to each chainID + pendingDatagrams, \* a function that assigns a set of pending datagrams to each chainID + relayerChainHeights, \* a function that assigns a height to each chainID turn - \* Instance of the environment, \* that takes care of the chain logic @@ -17,7 +16,7 @@ Env == INSTANCE environment pendingDatagrams <- pendingDatagrams, MaxHeight <- MaxHeight -vars == <> +vars == <> envVars == <> ChainIDs == Env!ChainIDs @@ -43,37 +42,84 @@ GetLatestHeight(chainID) == Env!GetLatestHeight(chainID) \* get the client height of the client for the counterparty chain on chainID GetCounterpartyClientHeight(chainID) == Env!GetCounterpartyClientHeight(chainID) +\* get the ID of the counterparty chain of chainID +GetCounterpartyChainID(chainID) == Env!GetCounterpartyChainID(chainID) + \* get the client ID of the client for chainID GetClientID(chainID) == Env!GetClientID(chainID) \* get the client ID of the client for the counterparty chain of chainID GetCounterpartyClientID(chainID) == Env!GetCounterpartyClientID(chainID) +\* returns true if the counterparty client is initialized on chainID +IsCounterpartyClientOnChain(chainID) == Env!IsCounterpartyClientOnChain(chainID) + +\* returns true if the counterparty client height on chainID is greater or equal than height +CounterpartyClientHeightUpdated(chainID, height) == Env!CounterpartyClientHeightUpdated(chainID, height) + (*************** Client datagrams -****************) +****************) -\* Compute client datagrams for clients on srcChainID for dstChainID -ClientDatagrams(srcChainID, dstChainID) == - LET dstChainHeight == GetLatestHeight(dstChainID) IN - LET dstClientHeight == GetCounterpartyClientHeight(srcChainID) IN - LET dstClientID == GetCounterpartyClientID(srcChainID) IN +\* Compute client datagrams for clients for srcChainID on dstChainID +\* Some client updates might trigger an update of the height that +\* the relayer stores for srcChainID +ClientDatagramsAndRelayerUpdate(srcChainID, dstChainID, relayer) == + LET srcChainHeight == GetLatestHeight(srcChainID) IN + LET srcClientHeight == GetCounterpartyClientHeight(dstChainID) IN + LET srcClientID == GetClientID(srcChainID) IN - LET srcDatagrams == - IF dstClientHeight = nullHeight - THEN \* the dst client does not exist on the src chain + \* check if the relayer chain height for srcChainID should be updated + LET srcRelayerChainHeight == + IF relayer[srcChainID] < srcChainHeight + THEN srcChainHeight + ELSE relayer[srcChainID] IN + + \* create an updated relayer + LET updatedRelayer == + [relayer EXCEPT ![srcChainID] = srcRelayerChainHeight] IN + + \* generate datagrams for dstChainID + LET dstDatagrams == + IF srcClientHeight = nullHeight + THEN \* the src client does not exist on dstChainID {[type |-> "CreateClient", - height |-> relayerClientHeights[dstClientID], - clientID |-> dstClientID]} - ELSE \* the dst client exists at the src chain - IF dstClientHeight < dstChainHeight - THEN \* the height of the dst client is smaller than the height of the dst chain + height |-> srcChainHeight, + clientID |-> srcClientID]} + ELSE \* the src client exists on dstChainID + IF srcClientHeight < srcChainHeight + THEN \* the height of the src client on dstChainID is smaller than the height of the src chain {[type |-> "ClientUpdate", - height |-> relayerClientHeights[dstClientID], - clientID |-> dstClientID]} + height |-> srcChainHeight, + clientID |-> srcClientID]} ELSE {} IN - srcDatagrams + [datagrams|-> dstDatagrams, relayerUpdate |-> updatedRelayer] + +\* Compute client datagrams for clients on srcChainID and on dstChainID, +\* as well as the relayer update triggered by creating client datagrams +ClientDatagrams(srcChainID, dstChainID) == + \* get client datagrams for dstChainID + \* and relayer update triggered by difference between the height of + \* srcChainID and the height that the relayer stores for srcChainID + LET dstClientDatagramsAndRelayerUpdate == + ClientDatagramsAndRelayerUpdate(srcChainID, dstChainID, relayerChainHeights) IN + LET dstClientDatagrams == + dstClientDatagramsAndRelayerUpdate.datagrams IN + LET dstRelayerUpdate == + dstClientDatagramsAndRelayerUpdate.relayerUpdate IN + + \* get client datagrams for srcChainID + \* and relayer update triggered by difference between the height of + \* dstChainID and the height that the relayer stores for dstChainID + LET srcClientDatagramsAndRelayerUpdate == + ClientDatagramsAndRelayerUpdate(dstChainID, srcChainID, dstRelayerUpdate) IN + LET srcClientDatagrams == + srcClientDatagramsAndRelayerUpdate.datagrams IN + LET srcRelayerUpdate == + srcClientDatagramsAndRelayerUpdate.relayerUpdate IN + + [src |-> srcClientDatagrams, dst |-> dstClientDatagrams, relayerUpdate |-> srcRelayerUpdate] (*************** Connection datagrams @@ -99,9 +145,8 @@ Compute pending datagrams \* TODO: Support the remaining datagrams PendingDatagrams(srcChainID, dstChainID) == \* ICS 002 : Clients - \* - Determine if light clients needs to be updated - LET clientDatagrams == [src |-> ClientDatagrams(srcChainID, dstChainID), - dst |-> ClientDatagrams(dstChainID, srcChainID)] IN + \* - Determine if light clients needs to be updated + LET clientDatagrams == ClientDatagrams(srcChainID, dstChainID) IN \* ICS3 : Connections \* - Determine if any connection handshakes are in progress @@ -111,18 +156,11 @@ PendingDatagrams(srcChainID, dstChainID) == \* - Determine if any packets, acknowledgements, or timeouts need to be relayed LET channelDatagrams == ChannelDatagrams(srcChainID, dstChainID) IN - [src |-> clientDatagrams.src - \union - connectionDatagrams.src - \union - channelDatagrams.src, - dst |-> clientDatagrams.dst - \union - connectionDatagrams.dst - \union - channelDatagrams.dst] - - + [src |-> clientDatagrams.src \union connectionDatagrams.src \union channelDatagrams.src, + dst |-> clientDatagrams.dst \union connectionDatagrams.dst \union channelDatagrams.dst, + relayerUpdate |-> clientDatagrams.relayerUpdate] + +\* TODO: pending datagrams per direction? (*************** Relayer actions @@ -131,9 +169,9 @@ Relayer actions \* Update the height of the relayer client for some chainID UpdateRelayerClients == \E chainID \in ChainIDs : - /\ relayerClientHeights[GetClientID(chainID)] < GetLatestHeight(chainID) - /\ relayerClientHeights' = [relayerClientHeights EXCEPT - ![GetClientID(chainID)] = GetLatestHeight(chainID) + /\ relayerChainHeights[chainID] < GetLatestHeight(chainID) + /\ relayerChainHeights' = [relayerChainHeights EXCEPT + ![chainID] = GetLatestHeight(chainID) ] /\ UNCHANGED <> @@ -147,28 +185,14 @@ Relay == \E srcChainID \in ChainIDs : \E dstChainID \in ChainIDs : /\ srcChainID /= dstChainID - /\ relayerClientHeights[GetClientID(srcChainID)] /= nullHeight - /\ relayerClientHeights[GetClientID(dstChainID)] /= nullHeight - /\ LET datagrams == PendingDatagrams(srcChainID, dstChainID) IN + /\ LET datagramsAndRelayerUpdate == PendingDatagrams(srcChainID, dstChainID) IN /\ pendingDatagrams' = [pendingDatagrams EXCEPT - ![srcChainID] = pendingDatagrams[srcChainID] \union datagrams.src, - ![dstChainID] = pendingDatagrams[dstChainID] \union datagrams.dst + ![srcChainID] = pendingDatagrams[srcChainID] \union datagramsAndRelayerUpdate.src, + ![dstChainID] = pendingDatagrams[dstChainID] \union datagramsAndRelayerUpdate.dst ] - /\ UNCHANGED <> - -\* might create state explosion -FaultyRelay == - \E srcChainID \in ChainIDs : - \E dstChainID \in ChainIDs : - LET srcFaultyDatagrams == CHOOSE src \in SUBSET(Datagrams) : TRUE IN - LET dstFaultyDatagrams == CHOOSE dst \in SUBSET(Datagrams) : TRUE IN - /\ pendingDatagrams' = - [pendingDatagrams EXCEPT - ![srcChainID] = pendingDatagrams[srcChainID] \union srcFaultyDatagrams, - ![dstChainID] = pendingDatagrams[dstChainID] \union dstFaultyDatagrams - ] - /\ UNCHANGED <> + /\ relayerChainHeights' = datagramsAndRelayerUpdate.relayerUpdate + /\ UNCHANGED chains (*************** Component actions @@ -182,7 +206,6 @@ Component actions Relayer == \/ UpdateRelayerClients \/ Relay -\* \/ FaultyRelay \/ UNCHANGED vars \* Environment @@ -190,7 +213,7 @@ Relayer == \* and leaves the relayer variable unchanged Environment == /\ Env!Next - /\ UNCHANGED relayerClientHeights + /\ UNCHANGED relayerChainHeights (*************** Specification @@ -199,12 +222,12 @@ Specification \* Initial state predicate \* Initially \* - it is either the relayer's or the environment's turn -\* - the relayer clients are uninitialized (i.e., their height -\* is nullHeight +\* - the relayer chain heights are uninitialized +\* (i.e., their height is nullHeight) \* - the environment is initialized, by Env!Init Init == /\ turn \in {"relayer", "environment"} - /\ relayerClientHeights = [clientID \in ClientIDs |-> nullHeight] + /\ relayerChainHeights = [chainID \in ChainIDs |-> nullHeight] /\ Env!Init \* Next state action @@ -221,13 +244,29 @@ Next == \* Fairness constraints Fairness == - /\ WF_<>(UpdateRelayerClients) - /\ WF_<>(Relay) + /\ WF_<>(UpdateRelayerClients) + /\ WF_<>(Relay) /\ Env!Fairness \* Spec formula Spec == Init /\ [][Next]_vars /\ Fairness +(************ +Helper operators used in properties +************) + +\* returns true if there is a "CreateClient" datagram +\* in the pending datagrams for chainID and height h +IsCreateClientInPendingDatagrams(chainID, clID, h) == + [type |-> "CreateClient", clientID |-> clID, height |-> h] + \in pendingDatagrams[chainID] + +\* returns true if there is a "ClientUpdate" datagram +\* in the pending datagrams for chainID and height h +IsClientUpdateInPendingDatagrams(chainID, clID, h) == + [type |-> "ClientUpdate", clientID |-> clID, height |-> h] + \in pendingDatagrams[chainID] + (************ Invariants ************) @@ -235,40 +274,92 @@ Invariants \* Type invariant TypeOK == /\ turn \in {"relayer", "environment"} - /\ relayerClientHeights \in [ClientIDs -> Heights \union {nullHeight}] + /\ relayerChainHeights \in [ChainIDs -> Heights \union {nullHeight}] /\ Env!TypeOK \* CreateClientInv \* if a "CreateClient" datagram is in pendingDatagrams for chainID, \* then the clientID of the datagram is the counterparty clientID for chainID CreateClientInv == - \A chainID \in ChainIDs : \A clID \in ClientIDs : \A h \in Heights : - [type |-> "CreateClient", clientID |-> clID, height |-> h] \in pendingDatagrams[chainID] - => clID = GetCounterpartyClientID(chainID) + \A chainID \in ChainIDs : \A clID \in ClientIDs : + ((\E h \in Heights : IsCreateClientInPendingDatagrams(chainID, clID, h)) + => (clID = GetCounterpartyClientID(chainID))) \* if a "ClientUpdate" datagram is in pendingDatagrams for chainID, \* then the clientID of the datagram is the counterparty clientID for chainID ClientUpdateInv == \A chainID \in ChainIDs : \A clID \in ClientIDs : \A h \in Heights : - [type |-> "ClientUpdate", clientID |-> clID, height |-> h] \in pendingDatagrams[chainID] + IsClientUpdateInPendingDatagrams(chainID, clID, h) => clID = GetCounterpartyClientID(chainID) -\* "CreateClient" datagrams are created -CreateClientIsGenerated == - [](\A chainID \in ChainIDs : - GetCounterpartyClientHeight(chainID) = nullHeight - => <>(\E h \in Heights : - [type |-> "CreateClient", clientID |-> GetCounterpartyClientID(chainID), height |-> h] - \in pendingDatagrams[chainID])) - \* Inv \* A conjunction of all invariants Inv == /\ TypeOK /\ CreateClientInv /\ ClientUpdateInv + +(************ +Properties +************) + +\* it ALWAYS holds that, for every chainID: +\* - if +\* * the counterparty client is not initialized +\* - then +\* * the relayer EVENTUALLY adds a "CreateClient" datagram in pending datagrams of chainID +CreateClientIsGenerated == + [](\A chainID \in ChainIDs : + GetCounterpartyClientHeight(chainID) = nullHeight + => <>(\E h \in Heights : IsCreateClientInPendingDatagrams(chainID, GetCounterpartyClientID(chainID), h))) + +\* it ALWAYS holds that, for every chainID: +\* - if +\* * there is a "CreateClient" datagram in pending datagrams of chainID for some height h +\* * a counterparty client does not exist on chainID +\* - then +\* * the counterparty client is EVENTUALLY created on chainID +ClientCreated == + [](\A chainID \in ChainIDs : + (/\ \E h \in Heights : IsCreateClientInPendingDatagrams(chainID, GetCounterpartyClientID(chainID), h) + /\ ~IsCounterpartyClientOnChain(chainID)) + => (<>(IsCounterpartyClientOnChain(chainID)))) + +\* it ALWAYS holds that, for every chainID: +\* - if +\* * the counterparty client on chainID has height smaller +\* than the height of chainID's counterparty chain +\* - then +\* * the relayer EVENTUALLY adds a "ClientUpdate" datagram in pending datagrams of chainID +ClientUpdateIsGenerated == + [](\A chainID \in ChainIDs : \A h1 \in Heights : + (/\ GetCounterpartyClientHeight(chainID) = h1 + /\ GetCounterpartyClientHeight(chainID) < GetLatestHeight(GetCounterpartyChainID(chainID))) + => <>(\E h2 \in Heights : + /\ h1 <= h2 + /\ IsClientUpdateInPendingDatagrams(chainID, GetCounterpartyClientID(chainID), h2))) + +\* it ALWAYS holds that, for every chainID: +\* - if +\* * there is a "ClientUpdate" datagram in pending datagrams of chainID +\* for height h +\* * the counterparty client height is smaller than h +\* - then +\* * the counterparty client height is EVENTUALLY udpated to at least h +ClientUpdated == + [](\A chainID \in ChainIDs : \A h \in Heights : + (/\ IsClientUpdateInPendingDatagrams(chainID, GetCounterpartyClientID(chainID), h) + /\ GetCounterpartyClientHeight(chainID) < h) + => (<>(CounterpartyClientHeightUpdated(chainID, h)))) + +\* for every chainID, it is always the case that the height of the chain +\* does not decrease +HeightsDontDecrease == + [](\A chainID \in ChainIDs : \A h \in Heights : + (chains[chainID].height = h) + => <>(chains[chainID].height >= h)) ============================================================================= \* Modification History -\* Last modified Wed Mar 25 17:49:56 CET 2020 by ilinastoilkovska +\* Last modified Thu Apr 02 18:33:51 CEST 2020 by ilinastoilkovska \* Created Fri Mar 06 09:23:12 CET 2020 by ilinastoilkovska From 287bd64409b58f886a9d48c0108249d6fff1f9ac Mon Sep 17 00:00:00 2001 From: Anca Zamfir Date: Fri, 10 Apr 2020 09:43:30 +0200 Subject: [PATCH 11/63] Add client queries to the relayer (#38) * debugging the consensus query with proof * started on client state * fixed unused warning * add height param to client query * cargo fmt * make proof option for client state * fix client state prove flag * cleanup queries * Try integration with tm-rs v0.33 to pick the new merkle proof * add validation function for common params * add utils * some cleanup and err handling * addressing Ismail's comments --- modules/Cargo.toml | 6 +- modules/src/ics02_client/state.rs | 22 ++ modules/src/ics07_tendermint/client_state.rs | 82 +++++++ .../src/ics07_tendermint/consensus_state.rs | 20 ++ modules/src/ics07_tendermint/mod.rs | 1 + modules/src/ics23_commitment/mod.rs | 7 +- modules/src/lib.rs | 3 + modules/src/path.rs | 16 ++ modules/src/path/cosmos.rs | 6 +- modules/src/path/ics.rs | 6 +- modules/src/test.rs | 30 +++ .../query/serialization/client_state.json | 11 + .../serialization/client_state_proof.json | 24 ++ .../query/serialization/consensus_state.json | 11 + .../serialization/consensus_state_proof.json | 24 ++ relayer/cli/Cargo.toml | 3 +- relayer/cli/src/commands.rs | 18 +- relayer/cli/src/commands/query.rs | 23 ++ .../cli/src/commands/query/query_client.rs | 216 ++++++++++++++++++ relayer/cli/src/commands/start.rs | 11 +- relayer/cli/src/commands/utils.rs | 11 + relayer/relay/Cargo.toml | 3 +- relayer/relay/src/chain.rs | 4 +- relayer/relay/src/chain/tendermint.rs | 4 + relayer/relay/src/query.rs | 3 +- .../relay/src/query/client_consensus_state.rs | 31 +-- relayer/relay/src/query/client_state.rs | 123 ++++++++++ 27 files changed, 680 insertions(+), 39 deletions(-) create mode 100644 modules/src/ics07_tendermint/client_state.rs create mode 100644 modules/src/test.rs create mode 100644 modules/src/tests/query/serialization/client_state.json create mode 100644 modules/src/tests/query/serialization/client_state_proof.json create mode 100644 modules/src/tests/query/serialization/consensus_state.json create mode 100644 modules/src/tests/query/serialization/consensus_state_proof.json create mode 100644 relayer/cli/src/commands/query.rs create mode 100644 relayer/cli/src/commands/query/query_client.rs create mode 100644 relayer/cli/src/commands/utils.rs create mode 100644 relayer/relay/src/query/client_state.rs diff --git a/modules/Cargo.toml b/modules/Cargo.toml index ce73546610..b983bae85e 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -15,9 +15,13 @@ default = ["paths-cosmos"] paths-cosmos = [] [dependencies] -tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git" } +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "tendermint/v0.33" } anomaly = "0.2.0" thiserror = "1.0.11" serde_derive = "1.0.104" serde = "1.0.104" + +[dev-dependencies] +serde_json = "1" + diff --git a/modules/src/ics02_client/state.rs b/modules/src/ics02_client/state.rs index aed8c3364a..fed45bcd4f 100644 --- a/modules/src/ics02_client/state.rs +++ b/modules/src/ics02_client/state.rs @@ -1,6 +1,7 @@ use super::client_type::ClientType; use crate::ics23_commitment::CommitmentRoot; +use crate::ics24_host::client::ClientId; use crate::Height; pub trait ConsensusState { @@ -18,3 +19,24 @@ pub trait ConsensusState { /// Performs basic validation of the consensus state fn validate_basic(&self) -> Result<(), Self::ValidationError>; } + +pub trait ClientState { + type ValidationError: std::error::Error; + + /// Client ID of this state + fn client_id(&self) -> ClientId; + + /// Type of client associated with this state (eg. Tendermint) + fn client_type(&self) -> ClientType; + + /// Height of consensus state + fn get_latest_height(&self) -> Height; + + /// Freeze status of the client + fn is_frozen(&self) -> bool; + + fn verify_client_consensus_state( + &self, + root: &CommitmentRoot, + ) -> Result<(), Self::ValidationError>; +} diff --git a/modules/src/ics07_tendermint/client_state.rs b/modules/src/ics07_tendermint/client_state.rs new file mode 100644 index 0000000000..23a62474ff --- /dev/null +++ b/modules/src/ics07_tendermint/client_state.rs @@ -0,0 +1,82 @@ +use crate::ics02_client::client_type::ClientType; +use crate::ics23_commitment::CommitmentRoot; + +use crate::ics07_tendermint::header::Header; +use crate::ics24_host::client::ClientId; +use serde_derive::{Deserialize, Serialize}; +use std::time::Duration; +use tendermint::lite::Header as liteHeader; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ClientState { + id: String, + trusting_period: Duration, + unbonding_period: Duration, + frozen_height: crate::Height, + latest_header: Header, +} + +impl ClientState { + pub fn new( + id: String, + trusting_period: Duration, + unbonding_period: Duration, + latest_header: Header, + frozen_height: crate::Height, + ) -> Self { + Self { + id, + trusting_period, + unbonding_period, + latest_header, + frozen_height, + } + } +} + +impl crate::ics02_client::state::ClientState for ClientState { + type ValidationError = crate::ics07_tendermint::error::Error; + + fn client_id(&self) -> ClientId { + self.id.parse().unwrap() + } + + fn client_type(&self) -> ClientType { + ClientType::Tendermint + } + + fn get_latest_height(&self) -> crate::Height { + self.latest_header.signed_header.header.height() + } + + fn is_frozen(&self) -> bool { + false + } + + fn verify_client_consensus_state( + &self, + _root: &CommitmentRoot, + ) -> Result<(), Self::ValidationError> { + unimplemented!() + } +} + +#[cfg(test)] +mod tests { + use crate::test::test_serialization_roundtrip; + use tendermint::rpc::endpoint::abci_query::AbciQuery; + + #[test] + fn serialization_roundtrip_no_proof() { + let json_data = include_str!("../tests/query/serialization/client_state.json"); + println!("json_data: {:?}", json_data); + test_serialization_roundtrip::(json_data); + } + + #[test] + fn serialization_roundtrip_with_proof() { + let json_data = include_str!("../tests/query/serialization/client_state_proof.json"); + println!("json_data: {:?}", json_data); + test_serialization_roundtrip::(json_data); + } +} diff --git a/modules/src/ics07_tendermint/consensus_state.rs b/modules/src/ics07_tendermint/consensus_state.rs index cec7332c7c..f6f60eebc9 100644 --- a/modules/src/ics07_tendermint/consensus_state.rs +++ b/modules/src/ics07_tendermint/consensus_state.rs @@ -46,3 +46,23 @@ impl crate::ics02_client::state::ConsensusState for ConsensusState { unimplemented!() } } + +#[cfg(test)] +mod tests { + use crate::test::test_serialization_roundtrip; + use tendermint::rpc::endpoint::abci_query::AbciQuery; + + #[test] + fn serialization_roundtrip_no_proof() { + let json_data = include_str!("../tests/query/serialization/consensus_state.json"); + println!("json_data: {:?}", json_data); + test_serialization_roundtrip::(json_data); + } + + #[test] + fn serialization_roundtrip_with_proof() { + let json_data = include_str!("../tests/query/serialization/consensus_state_proof.json"); + println!("json_data: {:?}", json_data); + test_serialization_roundtrip::(json_data); + } +} diff --git a/modules/src/ics07_tendermint/mod.rs b/modules/src/ics07_tendermint/mod.rs index 2b656579fa..d384cb4bc0 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_state; pub mod consensus_state; pub mod error; pub mod header; diff --git a/modules/src/ics23_commitment/mod.rs b/modules/src/ics23_commitment/mod.rs index b10701e993..92af1148a3 100644 --- a/modules/src/ics23_commitment/mod.rs +++ b/modules/src/ics23_commitment/mod.rs @@ -1,6 +1,7 @@ use serde_derive::{Deserialize, Serialize}; use crate::path::Path; +use tendermint::abci; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct CommitmentRoot; @@ -17,11 +18,11 @@ impl CommitmentPath { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct CommitmentProof; - +pub type CommitmentProof = abci::Proof; +/* impl CommitmentProof { pub fn from_bytes(_bytes: &[u8]) -> Self { todo!() } } +*/ diff --git a/modules/src/lib.rs b/modules/src/lib.rs index 69162648d5..f42f0519c9 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -27,3 +27,6 @@ pub mod path; /// Height of a block, same as in `tendermint` crate pub type Height = tendermint::lite::Height; + +#[cfg(test)] +mod test; diff --git a/modules/src/path.rs b/modules/src/path.rs index 065ea674d4..17a65ccb14 100644 --- a/modules/src/path.rs +++ b/modules/src/path.rs @@ -65,3 +65,19 @@ impl Path for ConsensusStatePath { paths::consensus_state_path(&self) } } + +pub struct ClientStatePath { + pub client_id: ClientId, +} + +impl ClientStatePath { + pub fn new(client_id: ClientId) -> Self { + Self { client_id } + } +} + +impl Path for ClientStatePath { + fn to_string(&self) -> String { + paths::client_state_path(&self) + } +} diff --git a/modules/src/path/cosmos.rs b/modules/src/path/cosmos.rs index bcac757bcf..84bdde30ce 100644 --- a/modules/src/path/cosmos.rs +++ b/modules/src/path/cosmos.rs @@ -1,4 +1,4 @@ -use super::{ConnectionPath, ConsensusStatePath}; +use super::{ClientStatePath, ConnectionPath, ConsensusStatePath}; pub fn connection_path(path: &ConnectionPath) -> String { format!("connection/{}", path.connection_id) @@ -7,3 +7,7 @@ pub fn connection_path(path: &ConnectionPath) -> String { pub fn consensus_state_path(path: &ConsensusStatePath) -> String { format!("consensusState/{}/{}", path.client_id, path.height) } + +pub fn client_state_path(path: &ClientStatePath) -> String { + format!("clientState/{}", path.client_id) +} diff --git a/modules/src/path/ics.rs b/modules/src/path/ics.rs index 42687fc711..8619459f1d 100644 --- a/modules/src/path/ics.rs +++ b/modules/src/path/ics.rs @@ -1,4 +1,4 @@ -use super::{ConnectionPath, ConsensusStatePath}; +use super::{ClientStatePath, ConnectionPath, ConsensusStatePath}; pub fn connection_path(_path: &ConnectionPath) -> String { todo!() @@ -7,3 +7,7 @@ pub fn connection_path(_path: &ConnectionPath) -> String { pub fn consensus_state_path(_path: &ConsensusStatePath) -> String { todo!() } + +pub fn client_state_path(_path: &ClientStatePath) -> String { + todo!() +} diff --git a/modules/src/test.rs b/modules/src/test.rs new file mode 100644 index 0000000000..3124af435a --- /dev/null +++ b/modules/src/test.rs @@ -0,0 +1,30 @@ +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::Debug; + +/// Test that a struct `T` can be: +/// +/// - parsed out of the provided JSON data +/// - serialized back to JSON +/// - parsed back from the serialized JSON of the previous step +/// - that the two parsed structs are equal according to their `PartialEq` impl +pub fn test_serialization_roundtrip(json_data: &str) +where + T: Debug + Serialize + DeserializeOwned, +{ + let parsed0 = serde_json::from_str::(json_data); + assert!(parsed0.is_ok()); + let parsed0 = parsed0.unwrap(); + + let serialized = serde_json::to_string(&parsed0); + assert!(serialized.is_ok()); + let serialized = serialized.unwrap(); + + let parsed1 = serde_json::from_str::(&serialized); + assert!(parsed1.is_ok()); + let parsed1 = parsed1.unwrap(); + println!("json_data0: {:?}", parsed0); + println!("json_data1: {:?}", parsed1); + + // TODO - fix PartialEq bound issue in AbciQuery + //assert_eq!(parsed0, parsed1); +} diff --git a/modules/src/tests/query/serialization/client_state.json b/modules/src/tests/query/serialization/client_state.json new file mode 100644 index 0000000000..36efe1086a --- /dev/null +++ b/modules/src/tests/query/serialization/client_state.json @@ -0,0 +1,11 @@ +{ + "code": 0, + "log": "", + "info": "", + "index": "0", + "key": "Y2xpZW50U3RhdGUvaWJjb25lY2xpZW50", + "value": "rwUhoYiWCgxpYmNvbmVjbGllbnQQgIDIkv+DkwIYgIDs2/7FnAMqiAUKgAQKxQIKAggKEgRpYmMxGBYiCwjMvIL0BRDIu58kKkgKICyeWhXpKZwrrUlzbaZ0DQnPqzkdNc8+k9eOogSLx43aEiQIARIgCFeoy6YD9BwuE0C1AU9BdEa2V51Cs28MXKewBA1rNtMyIBNgBAObhFbZAWB4KQGBQaC7Z/azZUkkSj2hMdZFFFppQiDses9b3+5uOFW4wZL6WWA0xtv9hTPe3Ib6BA7pzYQySUog7HrPW9/ubjhVuMGS+llgNMbb/YUz3tyG+gQO6c2EMklSIASAkbx93Cg/d7+/kdc8RNpYw9+KnLyGdAXYt/ParaIvWiBq+Q9GJMZNlWMQCpPikuUo+bJNzOKyjZSthVT8qdTQRmIgbjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB1yFPbqil8EwnDU5G93rMLZvekApgKBErUBCBYaSAogZik4C8oy55EdSL4OhMTIAy8jsDvCYiK6ZYH58Wv6Q88SJAgBEiBIpIMYxWV2OFZh72/ooILjIPvaHEMZ008+w1h+rpi7WiJnCAISFPbqil8EwnDU5G93rMLZvekApgKBGgsIzbyC9AUQqI/hRyJAbloumBKmq9nNQ22y/XoogOCrp33xeHjzA2JyuxpR2oAtL+8KGwKbcCJvAZjwpqOEEiRdXJnQ1rHrubVJhg3WBRKCAQo/ChT26opfBMJw1ORvd6zC2b3pAKYCgRIlFiTeZCBdmybLcqCWKo8EBc9ZOM+rYKVUx4QmxhMYv93tBdcNDxhkEj8KFPbqil8EwnDU5G93rMLZvekApgKBEiUWJN5kIF2bJstyoJYqjwQFz1k4z6tgpVTHhCbGExi/3e0F1w0PGGQ=", + "proof": null, + "height": "100", + "codespace": "" +} \ No newline at end of file diff --git a/modules/src/tests/query/serialization/client_state_proof.json b/modules/src/tests/query/serialization/client_state_proof.json new file mode 100644 index 0000000000..4803e7f15c --- /dev/null +++ b/modules/src/tests/query/serialization/client_state_proof.json @@ -0,0 +1,24 @@ +{ + "code": 0, + "log": "", + "info": "", + "index": "0", + "key": "Y2xpZW50U3RhdGUvaWJjb25lY2xpZW50", + "value": "rwUhoYiWCgxpYmNvbmVjbGllbnQQgIDIkv+DkwIYgIDs2/7FnAMqiAUKgAQKxQIKAggKEgRpYmMxGBYiCwjMvIL0BRDIu58kKkgKICyeWhXpKZwrrUlzbaZ0DQnPqzkdNc8+k9eOogSLx43aEiQIARIgCFeoy6YD9BwuE0C1AU9BdEa2V51Cs28MXKewBA1rNtMyIBNgBAObhFbZAWB4KQGBQaC7Z/azZUkkSj2hMdZFFFppQiDses9b3+5uOFW4wZL6WWA0xtv9hTPe3Ib6BA7pzYQySUog7HrPW9/ubjhVuMGS+llgNMbb/YUz3tyG+gQO6c2EMklSIASAkbx93Cg/d7+/kdc8RNpYw9+KnLyGdAXYt/ParaIvWiBq+Q9GJMZNlWMQCpPikuUo+bJNzOKyjZSthVT8qdTQRmIgbjQLnP+zepicpUTmu3gKLHiQHT+zNzh2hRGjBhevoB1yFPbqil8EwnDU5G93rMLZvekApgKBErUBCBYaSAogZik4C8oy55EdSL4OhMTIAy8jsDvCYiK6ZYH58Wv6Q88SJAgBEiBIpIMYxWV2OFZh72/ooILjIPvaHEMZ008+w1h+rpi7WiJnCAISFPbqil8EwnDU5G93rMLZvekApgKBGgsIzbyC9AUQqI/hRyJAbloumBKmq9nNQ22y/XoogOCrp33xeHjzA2JyuxpR2oAtL+8KGwKbcCJvAZjwpqOEEiRdXJnQ1rHrubVJhg3WBRKCAQo/ChT26opfBMJw1ORvd6zC2b3pAKYCgRIlFiTeZCBdmybLcqCWKo8EBc9ZOM+rYKVUx4QmxhMYv93tBdcNDxhkEj8KFPbqil8EwnDU5G93rMLZvekApgKBEiUWJN5kIF2bJstyoJYqjwQFz1k4z6tgpVTHhCbGExi/3e0F1w0PGGQ=", + "proof": { + "ops": [ + { + "type": "iavl:v", + "key": "Y2xpZW50U3RhdGUvaWJjb25lY2xpZW50", + "data": "6wEK6AEKKAgIEAwYHCogr722bXSL9572EU1Jgdz6re1sVtpscxSW4cjaLrCtKPoKKAgGEAcYHCIg1wO/S0DOyBX5/zwOTIQt5lY7SkEJUORfEfZtHH6ZcLcKKAgEEAQYHCIgvBVVJn1U3LKVuitwm6hAvnwujQDcyMOOp5YdNbwdyTEKKAgCEAIYFyIgcuZfF//lb+VDAKtkXsR/J2/QPYlaw8HvrSmNqDf1iVYaPgoYY2xpZW50U3RhdGUvaWJjb25lY2xpZW50EiDV6DHA5N7GJNbSj0hzc586MXPwXDj4H57rYWZesk2XjRgX" + }, + { + "type": "multistore", + "key": "aWJj", + "data": "CtUECi4KBGJhbmsSJgokCGQSIPfIBFZd98DupWwTml7I8kZFWKgF8ZTlslblitSLTJjqCi0KA2FjYxImCiQIZBIg3ZgdzN65c2/J62tIVCmQJwnN8Ho9dULDSL9dKEyA+cgKDwoHdXBncmFkZRIECgIIZAoxCgdzdGFraW5nEiYKJAhkEiCWTzW3aCWkLAyuZxi9z2CSXhmtmcRWEKi6K9ywkrVZMwoyCghzbGFzaGluZxImCiQIZBIgj6AcKVZzEJeL1r3wf9Qi5B53bjomfcn3g76fvuJn/FkKMAoGc3VwcGx5EiYKJAhkEiCRsUBZCybnNEYnaNqDTOO1pNEuAfGgY7pQMrgLCyUWBQotCgNnb3YSJgokCGQSIGDNE3wZYuysYWOJ1oA0gz8pIVCcLShapfUVOZfOlop0Ci0KA2liYxImCiQIZBIgWnpokBJ4idAoXzLzbjOOaBwU5E3oz4QJuAV8mShsqzwKLgoEbWFpbhImCiQIZBIgsZzwmLQ7PH1UeZ/vCUSqlQmfgt3CGfoMgJLkUqKCv0EKMAoGcGFyYW1zEiYKJAhkEiB8VIzExUHX+SvHZFz/P9NM9THnw/gTDDLVReuZX8htLgo2CgxkaXN0cmlidXRpb24SJgokCGQSIMaSvcpnTBH1nyPp/m3Ab+A/fowTOIWmuMtlbCda/2SsChAKCHRyYW5zZmVyEgQKAghkChAKCGV2aWRlbmNlEgQKAghkCi4KBG1pbnQSJgokCGQSIPvdoKLt1qhBYkRKjo39yCkQ0c1pOYM0kohjsVVGx9ae" + } + ] + }, + "height": "100", + "codespace": "" +} \ No newline at end of file diff --git a/modules/src/tests/query/serialization/consensus_state.json b/modules/src/tests/query/serialization/consensus_state.json new file mode 100644 index 0000000000..4d74e5e49a --- /dev/null +++ b/modules/src/tests/query/serialization/consensus_state.json @@ -0,0 +1,11 @@ +{ + "code": 0, + "log": "", + "info": "", + "index": "0", + "key": "Y29uc2Vuc3VzU3RhdGUvaWJjb25lY2xpZW50LzIy", + "value": "wAE7xH38CgsIzLyC9AUQyLufJBImzXAxMQogavkPRiTGTZVjEAqT4pLlKPmyTcziso2UrYVU/KnU0EYYFiKCAQo/ChT26opfBMJw1ORvd6zC2b3pAKYCgRIlFiTeZCBdmybLcqCWKo8EBc9ZOM+rYKVUx4QmxhMYv93tBdcNDxhkEj8KFPbqil8EwnDU5G93rMLZvekApgKBEiUWJN5kIF2bJstyoJYqjwQFz1k4z6tgpVTHhCbGExi/3e0F1w0PGGQ=", + "proof": null, + "height": "60295", + "codespace": "" +} \ No newline at end of file diff --git a/modules/src/tests/query/serialization/consensus_state_proof.json b/modules/src/tests/query/serialization/consensus_state_proof.json new file mode 100644 index 0000000000..bcc6c73138 --- /dev/null +++ b/modules/src/tests/query/serialization/consensus_state_proof.json @@ -0,0 +1,24 @@ +{ + "code": 0, + "log": "", + "info": "", + "index": "0", + "key": "Y29uc2Vuc3VzU3RhdGUvaWJjb25lY2xpZW50LzIy", + "value": "wAE7xH38CgsIzLyC9AUQyLufJBImzXAxMQogavkPRiTGTZVjEAqT4pLlKPmyTcziso2UrYVU/KnU0EYYFiKCAQo/ChT26opfBMJw1ORvd6zC2b3pAKYCgRIlFiTeZCBdmybLcqCWKo8EBc9ZOM+rYKVUx4QmxhMYv93tBdcNDxhkEj8KFPbqil8EwnDU5G93rMLZvekApgKBEiUWJN5kIF2bJstyoJYqjwQFz1k4z6tgpVTHhCbGExi/3e0F1w0PGGQ=", + "proof": { + "ops": [ + { + "type": "iavl:v", + "key": "Y29uc2Vuc3VzU3RhdGUvaWJjb25lY2xpZW50LzIy", + "data": "8QEK7gEKKAgIEAwYHCIgG9RAkJgHlxNjmyzOW6bUAidhiRSja0x6+GXCVENPG1oKKAgGEAUYFyIgwRns+dJvjf1Zk2BaFrXz8inPbvYHB7xx2HCy9ima5f8KKAgEEAMYFyogOr8EGajEV6fG5fzJ2fAAvVMgRLhdMJTzCPlogl9rxlIKKAgCEAIYFyIgcjzX/a+2bFbnNldpawQqZ+kYhIwz5r4wCUzuu1IFW04aRAoeY29uc2Vuc3VzU3RhdGUvaWJjb25lY2xpZW50LzIyEiAZ1uuG60K4NHJZZMuS9QX6o4eEhica5jIHYwflRiYkDBgX" + }, + { + "type": "multistore", + "key": "aWJj", + "data": "CvEECjAKBGJhbmsSKAomCIjYAxIg2MEyyonbZButYnvSRkf2bPQg+nqA+Am1MeDxG6F4p1UKLwoDYWNjEigKJgiI2AMSIN2YHczeuXNvyetrSFQpkCcJzfB6PXVCw0i/XShMgPnIChEKB3VwZ3JhZGUSBgoECIjYAwovCgNnb3YSKAomCIjYAxIgYM0TfBli7KxhY4nWgDSDPykhUJwtKFql9RU5l86WinQKLwoDaWJjEigKJgiI2AMSIFp6aJASeInQKF8y824zjmgcFORN6M+ECbgFfJkobKs8CjAKBG1haW4SKAomCIjYAxIgsZzwmLQ7PH1UeZ/vCUSqlQmfgt3CGfoMgJLkUqKCv0EKMwoHc3Rha2luZxIoCiYIiNgDEiCiBZoBLyDGj5euy3n33ik+SpqYK9eB5xbI+iY8ycYVbwo0CghzbGFzaGluZxIoCiYIiNgDEiAJz3gEYuIhdensHU3b5qH5ons2quepd6EaRgCHXab6PQoyCgZzdXBwbHkSKAomCIjYAxIglWLA5/THPTiTxAlaLHOBYFIzEJTmKPznItUwAc8zD+AKEgoIZXZpZGVuY2USBgoECIjYAwowCgRtaW50EigKJgiI2AMSIMS8dZ1j8F6JVVv+hB1rHBZC+gIFJxHan2hM8qDC64n/CjIKBnBhcmFtcxIoCiYIiNgDEiB8VIzExUHX+SvHZFz/P9NM9THnw/gTDDLVReuZX8htLgo4CgxkaXN0cmlidXRpb24SKAomCIjYAxIg3u/Nd4L+8LT8OXJCh14o8PHIJ/GLQwsmE7KYIl1GdSYKEgoIdHJhbnNmZXISBgoECIjYAw==" + } + ] + }, + "height": "60424", + "codespace": "" +} \ No newline at end of file diff --git a/relayer/cli/Cargo.toml b/relayer/cli/Cargo.toml index 2ab37e9e83..82930288e6 100644 --- a/relayer/cli/Cargo.toml +++ b/relayer/cli/Cargo.toml @@ -9,7 +9,8 @@ authors = [ [dependencies] relayer = { path = "../relay" } -tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git" } +relayer-modules = { path = "../../modules" } +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "tendermint/v0.33" } anomaly = "0.2.0" gumdrop = "0.7" diff --git a/relayer/cli/src/commands.rs b/relayer/cli/src/commands.rs index f1f7374a75..808ebc9d9e 100644 --- a/relayer/cli/src/commands.rs +++ b/relayer/cli/src/commands.rs @@ -7,10 +7,14 @@ mod config; mod light; +mod query; mod start; +mod utils; mod version; -use self::{config::ConfigCmd, light::LightCmd, start::StartCmd, version::VersionCmd}; +use self::{ + config::ConfigCmd, light::LightCmd, query::QueryCmd, start::StartCmd, version::VersionCmd, +}; use crate::config::Config; use abscissa_core::{Command, Configurable, FrameworkError, Help, Options, Runnable}; @@ -26,10 +30,6 @@ pub enum CliCmd { #[options(help = "get usage information")] Help(Help), - /// The `version` subcommand - #[options(help = "display version information")] - Version(VersionCmd), - /// The `start` subcommand #[options(help = "start the relayer")] Start(StartCmd), @@ -38,6 +38,14 @@ pub enum CliCmd { #[options(help = "manipulate the relayer configuration")] Config(ConfigCmd), + /// The `version` subcommand + #[options(help = "display version information")] + Version(VersionCmd), + + /// The `query` subcommand + #[options(help = "query state from chain")] + Query(QueryCmd), + /// The `light` subcommand #[options(help = "basic functionality for managing the lite clients")] Light(LightCmd), diff --git a/relayer/cli/src/commands/query.rs b/relayer/cli/src/commands/query.rs new file mode 100644 index 0000000000..5ecb5cf5d1 --- /dev/null +++ b/relayer/cli/src/commands/query.rs @@ -0,0 +1,23 @@ +//! `query` subcommand + +use abscissa_core::{Command, Options, Runnable}; + +mod query_client; + +/// `query` subcommand +#[derive(Command, Debug, Options, Runnable)] +pub enum QueryCmd { + /// The `query client` subcommand + #[options(help = "query client")] + Client(QueryClientCmds), +} + +#[derive(Command, Debug, Options, Runnable)] +pub enum QueryClientCmds { + /// The `query client` subcommand + #[options(help = "query client state")] + State(query_client::QueryClientStateCmd), + + #[options(help = "query client consensus")] + Consensus(query_client::QueryClientConsensusCmd), +} diff --git a/relayer/cli/src/commands/query/query_client.rs b/relayer/cli/src/commands/query/query_client.rs new file mode 100644 index 0000000000..ad16087b96 --- /dev/null +++ b/relayer/cli/src/commands/query/query_client.rs @@ -0,0 +1,216 @@ +use crate::prelude::*; + +use abscissa_core::{Command, Options, Runnable}; +use relayer::config::{ChainConfig, Config}; +use relayer::query::client_consensus_state::query_client_consensus_state; +use relayer::query::client_state::query_client_full_state; + +use relayer_modules::ics24_host::client::ClientId; + +use crate::commands::utils::block_on; +use relayer::chain::tendermint::TendermintChain; +use tendermint::chain::Id as ChainId; + +#[derive(Command, Debug, Options)] +pub struct QueryClientStateCmd { + #[options(free, help = "identifier of the chain to query")] + chain_id: Option, + + #[options(free, help = "identifier of the client to query")] + client_id: Option, + + #[options(help = "height of the state to query", short = "h")] + height: Option, + + #[options(help = "whether proof is required", short = "p")] + proof: Option, +} + +#[derive(Debug)] +struct QueryClientStateOptions { + client_id: ClientId, + height: u64, + proof: bool, +} + +impl QueryClientStateCmd { + fn validate_options( + &self, + config: &Config, + ) -> Result<(ChainConfig, QueryClientStateOptions), String> { + let (chain_config, client_id) = + validate_common_options(&self.chain_id, &self.client_id, config)?; + let opts = QueryClientStateOptions { + client_id: client_id.parse().unwrap(), + height: match self.height { + Some(h) => h, + None => 0 as u64, + }, + proof: match self.proof { + Some(proof) => proof, + None => true, + }, + }; + Ok((chain_config.clone(), opts)) + } +} + +impl Runnable for QueryClientStateCmd { + fn run(&self) { + let config = app_config(); + + let (chain_config, opts) = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + status_info!("Options", "{:?}", opts); + + // run with proof: + // cargo run --bin relayer -- -c simple_config.toml query client state ibc0 ibconeclient + // + // run without proof: + // cargo run --bin relayer -- -c simple_config.toml query client state ibc0 ibconeclient -p false + // + // Note: currently both fail in amino_unmarshal_binary_length_prefixed(). + // To test this start a Gaia node and configure a client using the go relayer. + let chain = TendermintChain::from_config(chain_config).unwrap(); + let res = block_on(query_client_full_state( + &chain, + opts.height, + opts.client_id.clone(), + opts.proof, + )); + match res { + Ok(cs) => status_info!("client state query result: ", "{:?}", cs.client_state), + Err(e) => status_info!("client state query error: ", "{:?}", e), + } + } +} + +#[derive(Command, Debug, Options)] +pub struct QueryClientConsensusCmd { + #[options(free, help = "identifier of the chain to query")] + chain_id: Option, + + #[options(free, help = "identifier of the client to query")] + client_id: Option, + + #[options(free, help = "height of the consensus state to query")] + consensus_height: Option, + + #[options(help = "height of the consensus state to query", short = "h")] + height: Option, + + #[options(help = "whether proof is required", short = "p")] + proof: Option, +} + +#[derive(Debug)] +struct QueryClientConsensusOptions { + client_id: ClientId, + consensus_height: u64, + height: u64, + proof: bool, +} + +impl QueryClientConsensusCmd { + fn validate_options( + &self, + config: &Config, + ) -> Result<(ChainConfig, QueryClientConsensusOptions), String> { + let (chain_config, client_id) = + validate_common_options(&self.chain_id, &self.client_id, config)?; + + match self.consensus_height { + Some(consensus_height) => { + let opts = QueryClientConsensusOptions { + client_id: client_id.parse().unwrap(), + consensus_height, + height: match self.height { + Some(h) => h, + None => 0 as u64, + }, + proof: match self.proof { + Some(proof) => proof, + None => true, + }, + }; + Ok((chain_config.clone(), opts)) + } + None => Err("missing client consensus height".to_string()), + } + } +} + +impl Runnable for QueryClientConsensusCmd { + fn run(&self) { + let config = app_config(); + + let (chain_config, opts) = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + status_info!("Options", "{:?}", opts); + + // run with proof: + // cargo run --bin relayer -- -c simple_config.toml query client consensus ibc0 ibconeclient 22 + // + // run without proof: + // cargo run --bin relayer -- -c simple_config.toml query client consensus ibc0 ibconeclient 22 -p false + // + // Note: currently both fail in amino_unmarshal_binary_length_prefixed(). + // To test this start a Gaia node and configure a client using the go relayer. + let chain = TendermintChain::from_config(chain_config).unwrap(); + let res = block_on(query_client_consensus_state( + &chain, + opts.height, + opts.client_id, + opts.consensus_height, + opts.proof, + )); + match res { + Ok(cs) => status_info!( + "client consensus state query result: ", + "{:?}", + cs.consensus_state + ), + Err(e) => status_info!("client consensus state query error: ", "{:?}", e), + } + } +} + +fn validate_common_options( + chain_id: &Option, + client_id: &Option, + config: &Config, +) -> Result<(ChainConfig, String), String> { + match (&chain_id, &client_id) { + (Some(chain_id), Some(client_id)) => { + let chain_config = config.chains.iter().find(|c| c.id == *chain_id); + + match chain_config { + Some(chain_config) => { + // check that the client_id is specified in one of the chain configurations + match config + .chains + .iter() + .find(|c| c.client_ids.contains(client_id)) + { + Some(_) => Ok((chain_config.clone(), client_id.parse().unwrap())), + None => Err(format!("cannot find client {} in config", client_id)), + } + } + None => Err(format!("cannot find chain {} in config", chain_id)), + } + } + + (None, _) => Err("missing chain identifier".to_string()), + (_, None) => Err("missing client identifier".to_string()), + } +} diff --git a/relayer/cli/src/commands/start.rs b/relayer/cli/src/commands/start.rs index 275da47160..59d7b07497 100644 --- a/relayer/cli/src/commands/start.rs +++ b/relayer/cli/src/commands/start.rs @@ -1,4 +1,3 @@ -use std::future::Future; use std::time::{Duration, SystemTime}; // use crate::application::APPLICATION; @@ -9,6 +8,7 @@ use abscissa_core::{Command, Options, Runnable}; use tendermint::lite::types::Header; +use crate::commands::utils::block_on; use relayer::chain::tendermint::TendermintChain; use relayer::chain::Chain; use relayer::client::Client; @@ -98,12 +98,3 @@ async fn create_client( Client::new(chain, store, trust_options).await.unwrap() } - -fn block_on(future: F) -> F::Output { - tokio::runtime::Builder::new() - .basic_scheduler() - .enable_all() - .build() - .unwrap() - .block_on(future) -} diff --git a/relayer/cli/src/commands/utils.rs b/relayer/cli/src/commands/utils.rs new file mode 100644 index 0000000000..c2c0d29130 --- /dev/null +++ b/relayer/cli/src/commands/utils.rs @@ -0,0 +1,11 @@ +use core::future::Future; + +/// block on future +pub fn block_on(future: F) -> F::Output { + tokio::runtime::Builder::new() + .basic_scheduler() + .enable_all() + .build() + .unwrap() + .block_on(future) +} diff --git a/relayer/relay/Cargo.toml b/relayer/relay/Cargo.toml index bb9d1f32af..ce46d31f04 100644 --- a/relayer/relay/Cargo.toml +++ b/relayer/relay/Cargo.toml @@ -9,8 +9,7 @@ authors = [ [dependencies] relayer-modules = { path = "../../modules" } -tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git" } - +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "tendermint/v0.33" } anomaly = "0.2.0" async-trait = "0.1.24" humantime-serde = "1.0.0" diff --git a/relayer/relay/src/chain.rs b/relayer/relay/src/chain.rs index 4909a0791a..58bab625f9 100644 --- a/relayer/relay/src/chain.rs +++ b/relayer/relay/src/chain.rs @@ -8,7 +8,7 @@ use ::tendermint::lite::types as tmlite; use ::tendermint::lite::{self, Height, TrustThresholdFraction}; use ::tendermint::rpc::Client as RpcClient; -use relayer_modules::ics02_client::state::ConsensusState; +use relayer_modules::ics02_client::state::{ClientState, ConsensusState}; use crate::config::ChainConfig; use crate::error; @@ -28,6 +28,8 @@ pub trait Chain { /// Type of consensus state for this chain type ConsensusState: ConsensusState + Serialize + DeserializeOwned; + type Type; + type ClientState: ClientState; /// Type of RPC requester (wrapper around low-level RPC client) for this chain type Requester: tmlite::Requester; diff --git a/relayer/relay/src/chain/tendermint.rs b/relayer/relay/src/chain/tendermint.rs index 4484e4833c..7a23e862e7 100644 --- a/relayer/relay/src/chain/tendermint.rs +++ b/relayer/relay/src/chain/tendermint.rs @@ -5,6 +5,8 @@ use tendermint::block::Header as TMHeader; use tendermint::lite::TrustThresholdFraction; use tendermint::rpc::Client as RpcClient; +use relayer_modules::ics02_client::client_type::ClientType; +use relayer_modules::ics07_tendermint::client_state::ClientState; use relayer_modules::ics07_tendermint::consensus_state::ConsensusState; use crate::client::rpc_requester::RpcRequester; @@ -34,10 +36,12 @@ impl TendermintChain { } impl Chain for TendermintChain { + type Type = ClientType; type Header = TMHeader; type Commit = TMCommit; type ConsensusState = ConsensusState; type Requester = RpcRequester; + type ClientState = ClientState; fn config(&self) -> &ChainConfig { &self.config diff --git a/relayer/relay/src/query.rs b/relayer/relay/src/query.rs index e9cdf62057..16fd19236b 100644 --- a/relayer/relay/src/query.rs +++ b/relayer/relay/src/query.rs @@ -7,6 +7,7 @@ use crate::chain::Chain; use crate::error; pub mod client_consensus_state; +pub mod client_state; /// The type of IBC response sent back for a given IBC `Query`. pub trait IbcResponse: Sized { @@ -72,5 +73,5 @@ where /// is_query_store_with_proofxpects a format like ///, /// where queryType must be "store" and subpath must be "key" to require a proof. fn is_query_store_with_proof(_path: &abci::Path) -> bool { - todo!() + false } diff --git a/relayer/relay/src/query/client_consensus_state.rs b/relayer/relay/src/query/client_consensus_state.rs index c2209a24b5..3a2a2c6ee9 100644 --- a/relayer/relay/src/query/client_consensus_state.rs +++ b/relayer/relay/src/query/client_consensus_state.rs @@ -18,16 +18,23 @@ pub struct QueryClientConsensusState { pub client_id: ClientId, pub consensus_height: Height, pub consensus_state_path: ConsensusStatePath, + pub prove: bool, marker: PhantomData, } impl QueryClientConsensusState { - pub fn new(chain_height: Height, client_id: ClientId, consensus_height: Height) -> Self { + pub fn new( + chain_height: Height, + client_id: ClientId, + consensus_height: Height, + prove: bool, + ) -> Self { Self { chain_height, client_id: client_id.clone(), consensus_height, consensus_state_path: ConsensusStatePath::new(client_id, consensus_height), + prove, marker: PhantomData, } } @@ -48,7 +55,7 @@ where } fn prove(&self) -> bool { - true + self.prove } fn data(&self) -> Vec { @@ -58,7 +65,7 @@ where pub struct ConsensusStateResponse { pub consensus_state: CS, - pub proof: CommitmentProof, + pub proof: Option, pub proof_path: CommitmentPath, pub proof_height: Height, } @@ -67,17 +74,15 @@ impl ConsensusStateResponse { pub fn new( client_id: ClientId, consensus_state: CS, - abci_proof: abci::Proof, + abci_proof: Option, proof_height: Height, ) -> Self { - let proof = CommitmentProof::from_bytes(abci_proof.as_ref()); - let proof_path = CommitmentPath::from_path(ConsensusStatePath::new(client_id, proof_height)); ConsensusStateResponse { consensus_state, - proof, + proof: abci_proof, proof_path, proof_height, } @@ -92,18 +97,18 @@ where query: QueryClientConsensusState, response: AbciQuery, ) -> Result { - match (response.value, response.proof) { - (Some(value), Some(proof)) => { + match (response.value, &response.proof) { + (Some(value), _) => { let consensus_state = amino_unmarshal_binary_length_prefixed(&value)?; Ok(ConsensusStateResponse::new( query.client_id, consensus_state, - proof, + response.proof, response.height.into(), )) } - _ => todo!(), + (None, _) => Err(error::Kind::Rpc.context("Bad response").into()), } } } @@ -113,12 +118,12 @@ pub async fn query_client_consensus_state( chain_height: Height, client_id: ClientId, consensus_height: Height, + prove: bool, ) -> Result, error::Error> where C: Chain, { - let query = QueryClientConsensusState::new(chain_height, client_id, consensus_height); - + let query = QueryClientConsensusState::new(chain_height, client_id, consensus_height, prove); ibc_query(chain, query).await } diff --git a/relayer/relay/src/query/client_state.rs b/relayer/relay/src/query/client_state.rs new file mode 100644 index 0000000000..f0f3d4b5af --- /dev/null +++ b/relayer/relay/src/query/client_state.rs @@ -0,0 +1,123 @@ +use std::marker::PhantomData; +use tendermint::rpc::endpoint::abci_query::AbciQuery; + +use tendermint::abci; + +use relayer_modules::ics02_client::state::ClientState; +use relayer_modules::ics23_commitment::{CommitmentPath, CommitmentProof}; +use relayer_modules::ics24_host::client::ClientId; +use relayer_modules::path::{ClientStatePath, Path}; +use relayer_modules::Height; + +use super::{ibc_query, IbcQuery, IbcResponse}; +use crate::chain::Chain; +use crate::error; + +pub struct QueryClientFullState { + pub chain_height: Height, + pub client_id: ClientId, + pub client_state_path: ClientStatePath, + pub prove: bool, + marker: PhantomData, +} + +impl QueryClientFullState { + pub fn new(chain_height: Height, client_id: ClientId, prove: bool) -> Self { + Self { + chain_height, + client_id: client_id.clone(), + client_state_path: ClientStatePath::new(client_id), + prove, + marker: PhantomData, + } + } +} + +impl IbcQuery for QueryClientFullState +where + CLS: ClientState, +{ + type Response = ClientFullStateResponse; + + fn path(&self) -> abci::Path { + "/store/ibc/key".parse().unwrap() + } + + fn height(&self) -> Height { + self.chain_height + } + + fn prove(&self) -> bool { + self.prove + } + + fn data(&self) -> Vec { + self.client_state_path.to_key().into() + } +} + +pub struct ClientFullStateResponse { + pub client_state: CLS, + pub proof: Option, + pub proof_path: CommitmentPath, + pub proof_height: Height, +} + +impl ClientFullStateResponse { + pub fn new( + client_id: ClientId, + client_state: CLS, + abci_proof: Option, + proof_height: Height, + ) -> Self { + let proof_path = CommitmentPath::from_path(ClientStatePath::new(client_id)); + + ClientFullStateResponse { + client_state, + proof: abci_proof, + proof_path, + proof_height, + } + } +} + +impl IbcResponse> for ClientFullStateResponse +where + CLS: ClientState, +{ + fn from_abci_response( + query: QueryClientFullState, + response: AbciQuery, + ) -> Result { + match (response.value, &response.proof) { + (Some(value), _) => { + let client_state = amino_unmarshal_binary_length_prefixed(&value)?; + + Ok(ClientFullStateResponse::new( + query.client_id, + client_state, + response.proof, + response.height.into(), + )) + } + (None, _) => Err(error::Kind::Rpc.context("Bad response").into()), + } + } +} + +pub async fn query_client_full_state( + chain: &C, + chain_height: Height, + client_id: ClientId, + prove: bool, +) -> Result, error::Error> +where + C: Chain, +{ + let query = QueryClientFullState::new(chain_height, client_id, prove); + ibc_query(chain, query).await +} + +fn amino_unmarshal_binary_length_prefixed(_bytes: &[u8]) -> Result { + todo!() +} From 8cd5bd0cda0d4da30594fc6ed8139cd06234d3f4 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 14 Apr 2020 12:17:39 +0200 Subject: [PATCH 12/63] Perform bisection when verifying headers using the light client (#51) * Perform bisection when verifying new headers in client * Fix typo in README * Tick interval at the beginning of the client loop * Do not update the light client upon creation * Improve client logs --- README.md | 2 +- relayer/cli/Cargo.toml | 6 +- relayer/cli/src/commands/start.rs | 69 +++++++++++------- relayer/relay/src/chain.rs | 9 +-- relayer/relay/src/chain/tendermint.rs | 2 - relayer/relay/src/client.rs | 100 +++++++++++--------------- 6 files changed, 94 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 0be345b1e0..b7601ea453 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ configure and spawn two light clients for this chain with the following commands 4. Start the light clients and a dummy relayer thread: ```bash - ibc-rs $ cargo run --bin relayer -- -c ./relayer/relay/tests/config/fixtures/relayer_conf_example.toml run + ibc-rs $ cargo run --bin relayer -- -c ./relayer/relay/tests/config/fixtures/relayer_conf_example.toml start ``` **Note:** Add a `-v` flag to the commands above to see detailed log output, eg. `cargo run --bin relayer -- -v -c ./relayer/relay/tests/config/fixtures/relayer_conf_example.toml run` diff --git a/relayer/cli/Cargo.toml b/relayer/cli/Cargo.toml index 82930288e6..649f55ea8a 100644 --- a/relayer/cli/Cargo.toml +++ b/relayer/cli/Cargo.toml @@ -12,14 +12,14 @@ relayer = { path = "../relay" } relayer-modules = { path = "../../modules" } tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "tendermint/v0.33" } +abscissa_tokio = "0.5.1" anomaly = "0.2.0" gumdrop = "0.7" serde = { version = "1", features = ["serde_derive"] } thiserror = "1" -abscissa_tokio = "0.5.1" -tokio = "0.2.13" -tracing-subscriber = "0.2.3" +tokio = { version = "0.2.13", features = ["rt-util"] } tracing = "0.1.13" +tracing-subscriber = "0.2.3" [dependencies.abscissa_core] version = "0.5.2" diff --git a/relayer/cli/src/commands/start.rs b/relayer/cli/src/commands/start.rs index 59d7b07497..11f1cf0f70 100644 --- a/relayer/cli/src/commands/start.rs +++ b/relayer/cli/src/commands/start.rs @@ -27,64 +27,85 @@ impl Runnable for StartCmd { debug!("launching 'start' command"); - block_on(async { + // Spawn all tasks on the same thread that calls `block_on`, ie. the main thread. + // This allows us to spawn tasks which do not implement `Send`, + // like the light client task. + let local = tokio::task::LocalSet::new(); + + block_on(local.run_until(async move { for chain_config in config.chains { info!(chain.id = %chain_config.id, "spawning light client"); + tokio::task::spawn_local(spawn_client(chain_config)); + } - let _handle = tokio::spawn(async move { - let client = create_client(chain_config).await; - let trusted_state = client.last_trusted_state().unwrap(); + info!("starting relayer"); + relayer_task().await + })); + } +} - info!( - chain.id = %client.chain().id(), - "Spawned new client now at trusted state: {} at height {}", - trusted_state.last_header().header().hash(), - trusted_state.last_header().header().height(), - ); +async fn spawn_client(chain_config: ChainConfig) { + status_info!( + "Relayer", + "spawning light client for chain {}", + chain_config.id + ); + + let client = create_client(chain_config).await; + tokio::task::spawn_local(client_task(client)) + .await + .expect("could not spawn client task") +} - update_headers(client).await; - }); - } +async fn client_task(client: Client>) { + let trusted_state = client.last_trusted_state().unwrap(); - start_relayer().await - }) - } + status_ok!( + client.chain().id(), + "spawned new client now at trusted state: {} at height {}", + trusted_state.last_header().header().hash(), + trusted_state.last_header().header().height(), + ); + + update_client(client).await; } -async fn start_relayer() { +async fn relayer_task() { let mut interval = tokio::time::interval(Duration::from_secs(3)); loop { - info!(target: "relayer_cli::relayer", "Relayer is running"); + info!(target: "relayer_cli::relayer", "relayer is running"); interval.tick().await; } } -async fn update_headers>(mut client: Client) { +async fn update_client>(mut client: Client) { debug!(chain.id = %client.chain().id(), "updating headers"); let mut interval = tokio::time::interval(Duration::from_secs(3)); loop { + interval.tick().await; + + info!(chain.id = %client.chain().id(), "updating client"); + let result = client.update(SystemTime::now()).await; match result { Ok(Some(trusted_state)) => info!( chain.id = %client.chain().id(), - "Updated to trusted state: {} at height {}", + "client updated to trusted state: {} at height {}", trusted_state.header().hash(), trusted_state.header().height() ), Ok(None) => { - warn!(chain.id = %client.chain().id(), "Ignoring update to a previous state") + warn!(chain.id = %client.chain().id(), "ignoring update to a previous state") } Err(err) => { - error!(chain.id = %client.chain().id(), "Error when updating headers: {}", err) + error!(chain.id = %client.chain().id(), "error when updating headers: {}", err) } } - - interval.tick().await; } } diff --git a/relayer/relay/src/chain.rs b/relayer/relay/src/chain.rs index 58bab625f9..b522723591 100644 --- a/relayer/relay/src/chain.rs +++ b/relayer/relay/src/chain.rs @@ -21,14 +21,15 @@ pub type ValidatorSet = <::Commit as tmlite::Commit /// Defines a blockchain as understood by the relayer pub trait Chain { /// Type of headers for this chain - type Header: tmlite::Header + Serialize + DeserializeOwned; + type Header: tmlite::Header + Send + Sync + Serialize + DeserializeOwned; /// Type of commits for this chain - type Commit: tmlite::Commit + Serialize + DeserializeOwned; + type Commit: tmlite::Commit + Send + Sync + Serialize + DeserializeOwned; /// Type of consensus state for this chain - type ConsensusState: ConsensusState + Serialize + DeserializeOwned; - type Type; + type ConsensusState: ConsensusState + Send + Sync + Serialize + DeserializeOwned; + + /// Type of the client state for this chain type ClientState: ClientState; /// Type of RPC requester (wrapper around low-level RPC client) for this chain diff --git a/relayer/relay/src/chain/tendermint.rs b/relayer/relay/src/chain/tendermint.rs index 7a23e862e7..08689beaee 100644 --- a/relayer/relay/src/chain/tendermint.rs +++ b/relayer/relay/src/chain/tendermint.rs @@ -5,7 +5,6 @@ use tendermint::block::Header as TMHeader; use tendermint::lite::TrustThresholdFraction; use tendermint::rpc::Client as RpcClient; -use relayer_modules::ics02_client::client_type::ClientType; use relayer_modules::ics07_tendermint::client_state::ClientState; use relayer_modules::ics07_tendermint::consensus_state::ConsensusState; @@ -36,7 +35,6 @@ impl TendermintChain { } impl Chain for TendermintChain { - type Type = ClientType; type Header = TMHeader; type Commit = TMCommit; type ConsensusState = ConsensusState; diff --git a/relayer/relay/src/client.rs b/relayer/relay/src/client.rs index 013adaf51d..c8aa11c58e 100644 --- a/relayer/relay/src/client.rs +++ b/relayer/relay/src/client.rs @@ -2,12 +2,12 @@ use std::cmp::Ordering; use std::time::{Duration, SystemTime}; use anomaly::fail; -use tracing::debug; +use tracing::{debug, info}; use tendermint::lite::types::{Commit, Header as _, Requester, ValidatorSet as _}; use tendermint::lite::{SignedHeader, TrustThresholdFraction, TrustedState}; -use crate::chain::{self, ValidatorSet}; +use crate::chain; use crate::error; use crate::store::{self, StoreHeight}; @@ -93,10 +93,6 @@ where client.init_with_trust_options(trust_options).await?; } - // Perform an update already, to make sure the client is up-to-date. - // TODO: Should we leave this up to the responsibility of the caller? - let _ = client.update(SystemTime::now()).await?; - Ok(client) } @@ -135,16 +131,8 @@ where .await .map_err(|e| error::Kind::LightClient.context(e))?; - let latest_validator_set = self - .chain - .requester() - .validator_set(latest_header.header().height()) - .await - .map_err(|e| error::Kind::LightClient.context(e))?; - if latest_header.header().height() > last_trusted_height { - self.verify_header(&latest_header, &latest_validator_set, now) - .await?; + self.verify_header(&latest_header, now).await?; Ok(Some(latest_header)) } else { @@ -162,15 +150,14 @@ where /// the stored hash, we fail with an error. /// /// Otherwise, and only if we already have a trusted state, - /// we then pull the next validator set (w.r.t to the given header) - /// and attempt to verify the new header against it. - /// If that succeeds we update the trusted store and our last trusted state. + /// we attempt bisection for the new header at the given height. + /// If this succeeds, we add the new trusted states to the store and + /// update our last trusted state. /// - /// If there is no current trusted state, we fail with an error. + /// In any other case, we fail with an error. async fn verify_header( &mut self, new_header: &SignedHeader, - new_validator_set: &ValidatorSet, now: SystemTime, ) -> Result<(), error::Error> { let in_store = self @@ -192,40 +179,26 @@ where } } - // If we already have a trusted state, we then pull the next validator set of - // the header we were given to verify, and attempt to verify the new - // header against it. + // If we already have a trusted state, we attempt bisection for the + // new header at the given height. If it succeeds, we add the new + // trusted states to the store and update our last trusted state. if let Some(ref last_trusted_state) = self.last_trusted_state { - let next_height = new_header - .header() - .height() - .checked_add(1) - .expect("height overflow"); - - let new_next_validator_set = self - .chain - .requester() - .validator_set(next_height) - .await - .map_err(|e| error::Kind::LightClient.context(e))?; - - tendermint::lite::verifier::verify_single( + let new_header_height = new_header.header().height(); + + let new_trusted_states = tendermint::lite::verifier::verify_bisection( last_trusted_state.clone(), - &new_header, - &new_validator_set, - &new_next_validator_set, + new_header_height, self.trust_threshold, self.trusting_period, now, + self.chain.requester(), ) + .await .map_err(|e| error::Kind::LightClient.context(e))?; - // TODO: Compare new header with witnesses (?) - - let new_trusted_state = - TrustedState::new(new_header.clone(), new_validator_set.clone()); - - self.update_trusted_state(new_trusted_state)?; + for new_trusted_state in new_trusted_states { + self.update_trusted_state(new_trusted_state)?; + } Ok(()) } else { @@ -313,10 +286,9 @@ where chain.id = %self.chain.id(), trust_options.height, trusted_header.height = trusted_header.header().height(), - "trusted options height is greater than header height in trust store", + "trusted options height is greater than trusted header height", ); - // TODO: Fetch from primary (?) self.chain .requester() .signed_header(trust_options.height) @@ -330,7 +302,7 @@ where chain.id = %self.chain.id(), trust_options.height, trusted_header.height = trusted_header.header().height(), - "trusted options height is equal to header height in trust store", + "trusted options height is equal to trusted header height", ); trust_options.hash @@ -340,11 +312,19 @@ where chain.id = %self.chain.id(), trust_options.height, trusted_header.height = trusted_header.header().height(), - "trusted options height is lesser than header height in trust store. TODO: rollback", + "trusted options height is lesser than trusted header height", + ); + + info!( + chain.id = %self.chain.id(), + old = %trust_options.height, + trusted_header.height = %trusted_header.header().height(), + trusted_header.hash = %trusted_header.header().hash(), + "client initialized with old header (trusted header is more recent)", ); - // TODO: Implement rollback - // + // TODO: Rollback: Remove all headers in (trust_options.height, trusted_header.height] + trust_options.hash } }; @@ -363,17 +343,17 @@ where chain.id = %self.chain.id(), primary.hash = %primary_hash, trusted_header.hash = %trusted_header.header().hash(), - "hash do not match! TODO: cleanup", + "hashes do not match", + ); + } else { + debug!( + chain.id = %self.chain.id(), + primary.hash = %primary_hash, + trusted_header.hash = %trusted_header.header().hash(), + "hashes match", ); } - debug!( - chain.id = %self.chain.id(), - primary.hash = %primary_hash, - trusted_header.hash = %trusted_header.header().hash(), - "headers hashes match", - ); - Ok(()) } From 07a58a05e23fdf6e9b0d29c2fcf512bbd668d525 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Thu, 16 Apr 2020 16:21:37 +0200 Subject: [PATCH 13/63] Start light clients from trusted store by default. (#54) This commit also adds a `--reset/-r` option to force all clients to start from the configured trust options. --- relayer/cli/src/commands/start.rs | 28 +++++++++--- relayer/relay/src/client.rs | 72 +++++++++++++++---------------- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/relayer/cli/src/commands/start.rs b/relayer/cli/src/commands/start.rs index 11f1cf0f70..90291d1658 100644 --- a/relayer/cli/src/commands/start.rs +++ b/relayer/cli/src/commands/start.rs @@ -16,7 +16,10 @@ use relayer::config::ChainConfig; use relayer::store::Store; #[derive(Command, Debug, Options)] -pub struct StartCmd {} +pub struct StartCmd { + #[options(help = "reset state from trust options", short = "r")] + reset: bool, +} impl Runnable for StartCmd { fn run(&self) { @@ -35,7 +38,7 @@ impl Runnable for StartCmd { block_on(local.run_until(async move { for chain_config in config.chains { info!(chain.id = %chain_config.id, "spawning light client"); - tokio::task::spawn_local(spawn_client(chain_config)); + tokio::task::spawn_local(spawn_client(chain_config, self.reset)); } info!("starting relayer"); @@ -44,14 +47,14 @@ impl Runnable for StartCmd { } } -async fn spawn_client(chain_config: ChainConfig) { +async fn spawn_client(chain_config: ChainConfig, reset: bool) { status_info!( "Relayer", "spawning light client for chain {}", - chain_config.id + chain_config.id, ); - let client = create_client(chain_config).await; + let client = create_client(chain_config, reset).await; tokio::task::spawn_local(client_task(client)) .await .expect("could not spawn client task") @@ -111,11 +114,24 @@ async fn update_client>(mut client: Client) { async fn create_client( chain_config: ChainConfig, + reset: bool, ) -> Client> { + let id = chain_config.id; let chain = TendermintChain::from_config(chain_config).unwrap(); let store = relayer::store::persistent(format!("store_{}.db", chain.id())).unwrap(); //FIXME: unwrap let trust_options = store.get_trust_options().unwrap(); // FIXME: unwrap - Client::new(chain, store, trust_options).await.unwrap() + if reset { + info!(chain.id = %id, "resetting client to trust options state"); + let client = Client::new_from_trust_options(chain, store, &trust_options); + client.await.unwrap() // FIXME: unwrap + } else { + info!(chain.id = %chain.id(), "starting client from stored trusted state"); + let client = Client::new_from_trusted_store(chain, store, &trust_options).unwrap(); // FIXME: unwrap + if client.last_trusted_state().is_none() { + error!(chain.id = %id, "cannot find stored trusted state, unable to start client"); + } + client + } } diff --git a/relayer/relay/src/client.rs b/relayer/relay/src/client.rs index c8aa11c58e..ac7925f00c 100644 --- a/relayer/relay/src/client.rs +++ b/relayer/relay/src/client.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use std::time::{Duration, SystemTime}; use anomaly::fail; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; use tendermint::lite::types::{Commit, Header as _, Requester, ValidatorSet as _}; use tendermint::lite::{SignedHeader, TrustThresholdFraction, TrustedState}; @@ -52,24 +52,49 @@ where Chain: chain::Chain, Store: store::Store, { - /// Creates a new `Client` for the given `chain`, storing headers + /// Create a new `Client` for the given `Chain` with the given trusted store + /// and trust options, and try to restore the last trusted state from the trusted store. + pub fn new_from_trusted_store( + chain: Chain, + trusted_store: Store, + trust_options: &TrustOptions, + ) -> Result { + debug!( + chain.id = %chain.id(), + "initializing client from trusted store", + ); + + let mut client = Self { + chain, + trusted_store, + trusting_period: trust_options.trusting_period, + trust_threshold: trust_options.trust_threshold, + last_trusted_state: None, + }; + + client.restore_trusted_state()?; + + Ok(client) + } + + /// Create a new `Client` for the given `chain`, storing headers /// in the given `trusted_store`, and verifying them with the /// given `trust_options`. /// /// This method is async because it needs to pull the latest header /// from the chain, verify it, and store it. - pub async fn new( + pub async fn new_from_trust_options( chain: Chain, trusted_store: Store, - trust_options: TrustOptions, + trust_options: &TrustOptions, ) -> Result { let mut client = Self::new_from_trusted_store(chain, trusted_store, &trust_options)?; - // If we managed to pull and verify a header from the chain already + // If we already have a last trusted state if let Some(ref trusted_state) = client.last_trusted_state { debug!("found last trusted state"); - // Check that this header can be trusted with the given trust options + // Check that the last trusted header can actually be trusted with the given trust options client .check_trusted_header(trusted_state.last_header(), &trust_options) .await?; @@ -209,31 +234,6 @@ where } } - /// Create a new client with the given trusted store and trust options, - /// and try to restore the last trusted state from the trusted store. - fn new_from_trusted_store( - chain: Chain, - trusted_store: Store, - trust_options: &TrustOptions, - ) -> Result { - debug!( - chain.id = %chain.id(), - "initializing client from trusted store", - ); - - let mut client = Self { - chain, - trusted_store, - trusting_period: trust_options.trusting_period, - trust_threshold: trust_options.trust_threshold, - last_trusted_state: None, - }; - - client.restore_trusted_state()?; - - Ok(client) - } - /// Restore the last trusted state from the state, by asking for /// its last stored height, without any verification. fn restore_trusted_state(&mut self) -> Result<(), error::Error> { @@ -339,25 +339,25 @@ where if primary_hash != trusted_header.header().hash() { // TODO: Implement cleanup - debug!( + warn!( chain.id = %self.chain.id(), primary.hash = %primary_hash, trusted_header.hash = %trusted_header.header().hash(), - "hashes do not match", + "trusted header hash and primary header hash do not match, rolling back (TODO)", ); } else { debug!( chain.id = %self.chain.id(), primary.hash = %primary_hash, trusted_header.hash = %trusted_header.header().hash(), - "hashes match", + "trusted header hash and primary header hash match", ); } Ok(()) } - /// Init this client with the given trust options. + /// Initialize this client with the given trust options. /// /// This pulls the header and validator set at the height specified in /// the trust options, and checks their hashes against the hashes @@ -371,7 +371,7 @@ where /// update the last trusted state with it. async fn init_with_trust_options( &mut self, - trust_options: TrustOptions, + trust_options: &TrustOptions, ) -> Result<(), error::Error> { debug!("initialize client with given trust_options"); From b47619f6bc2bccdcc15da0348198a6b1f04538ab Mon Sep 17 00:00:00 2001 From: Anca Zamfir Date: Thu, 16 Apr 2020 16:34:49 +0200 Subject: [PATCH 14/63] Move the query request and response details to modules (#57) --- modules/src/error.rs | 17 +++ modules/src/ics02_client/mod.rs | 1 + .../src/ics02_client/query.rs | 129 ++++++++++++++---- modules/src/lib.rs | 2 + modules/src/query.rs | 35 +++++ relayer/cli/src/commands/query.rs | 6 +- .../query/{query_client.rs => client.rs} | 3 +- relayer/relay/src/query.rs | 39 +----- relayer/relay/src/query/client.rs | 36 +++++ relayer/relay/src/query/client_state.rs | 123 ----------------- 10 files changed, 201 insertions(+), 190 deletions(-) create mode 100644 modules/src/error.rs rename relayer/relay/src/query/client_consensus_state.rs => modules/src/ics02_client/query.rs (51%) create mode 100644 modules/src/query.rs rename relayer/cli/src/commands/query/{query_client.rs => client.rs} (98%) create mode 100644 relayer/relay/src/query/client.rs delete mode 100644 relayer/relay/src/query/client_state.rs diff --git a/modules/src/error.rs b/modules/src/error.rs new file mode 100644 index 0000000000..9f728127c9 --- /dev/null +++ b/modules/src/error.rs @@ -0,0 +1,17 @@ +use anomaly::{BoxError, Context}; +use thiserror::Error; + +pub type Error = anomaly::Error; + +#[derive(Clone, Debug, Error)] +pub enum Kind { + /// RPC error (typically raised by the RPC client or the RPC requester) + #[error("RPC error")] + Rpc, +} + +impl Kind { + pub fn context(self, source: impl Into) -> Context { + Context::new(self, Some(source.into())) + } +} diff --git a/modules/src/ics02_client/mod.rs b/modules/src/ics02_client/mod.rs index a9923d883b..9cb955cc70 100644 --- a/modules/src/ics02_client/mod.rs +++ b/modules/src/ics02_client/mod.rs @@ -4,4 +4,5 @@ pub mod client_type; pub mod error; pub mod header; pub mod msgs; +pub mod query; pub mod state; diff --git a/relayer/relay/src/query/client_consensus_state.rs b/modules/src/ics02_client/query.rs similarity index 51% rename from relayer/relay/src/query/client_consensus_state.rs rename to modules/src/ics02_client/query.rs index 3a2a2c6ee9..3b83dff054 100644 --- a/relayer/relay/src/query/client_consensus_state.rs +++ b/modules/src/ics02_client/query.rs @@ -3,15 +3,110 @@ use tendermint::rpc::endpoint::abci_query::AbciQuery; use tendermint::abci; -use relayer_modules::ics02_client::state::ConsensusState; -use relayer_modules::ics23_commitment::{CommitmentPath, CommitmentProof}; -use relayer_modules::ics24_host::client::ClientId; -use relayer_modules::path::{ConsensusStatePath, Path}; -use relayer_modules::Height; - -use super::{ibc_query, IbcQuery, IbcResponse}; -use crate::chain::Chain; +use crate::ics23_commitment::{CommitmentPath, CommitmentProof}; + use crate::error; +use crate::ics02_client::state::{ClientState, ConsensusState}; +use crate::ics24_host::client::ClientId; +use crate::path::{ClientStatePath, ConsensusStatePath, Path}; +use crate::query::{IbcQuery, IbcResponse}; +use crate::Height; + +pub struct QueryClientFullState { + pub chain_height: Height, + pub client_id: ClientId, + pub client_state_path: ClientStatePath, + pub prove: bool, + marker: PhantomData, +} + +impl QueryClientFullState { + pub fn new(chain_height: Height, client_id: ClientId, prove: bool) -> Self { + Self { + chain_height, + client_id: client_id.clone(), + client_state_path: ClientStatePath::new(client_id), + prove, + marker: PhantomData, + } + } +} + +impl IbcQuery for QueryClientFullState +where + CLS: ClientState, +{ + type Response = ClientFullStateResponse; + + fn path(&self) -> abci::Path { + "/store/ibc/key".parse().unwrap() + } + + fn height(&self) -> Height { + self.chain_height + } + + fn prove(&self) -> bool { + self.prove + } + + fn data(&self) -> Vec { + self.client_state_path.to_key().into() + } +} + +pub struct ClientFullStateResponse { + pub client_state: CLS, + pub proof: Option, + pub proof_path: CommitmentPath, + pub proof_height: Height, +} + +impl ClientFullStateResponse { + pub fn new( + client_id: ClientId, + client_state: CLS, + abci_proof: Option, + proof_height: Height, + ) -> Self { + let proof_path = CommitmentPath::from_path(ClientStatePath::new(client_id)); + + ClientFullStateResponse { + client_state, + proof: abci_proof, + proof_path, + proof_height, + } + } +} + +impl IbcResponse> for ClientFullStateResponse +where + CLS: ClientState, +{ + fn from_abci_response( + query: QueryClientFullState, + response: AbciQuery, + ) -> Result { + match (response.value, &response.proof) { + (Some(value), _) => { + let client_state = amino_unmarshal_binary_length_prefixed(&value)?; + + Ok(ClientFullStateResponse::new( + query.client_id, + client_state, + response.proof, + response.height.into(), + )) + } + (None, _) => Err(error::Kind::Rpc.context("Bad response").into()), + } + } +} + +fn amino_unmarshal_binary_length_prefixed(_bytes: &[u8]) -> Result { + todo!() +} pub struct QueryClientConsensusState { pub chain_height: Height, @@ -112,21 +207,3 @@ where } } } - -pub async fn query_client_consensus_state( - chain: &C, - chain_height: Height, - client_id: ClientId, - consensus_height: Height, - prove: bool, -) -> Result, error::Error> -where - C: Chain, -{ - let query = QueryClientConsensusState::new(chain_height, client_id, consensus_height, prove); - ibc_query(chain, query).await -} - -fn amino_unmarshal_binary_length_prefixed(_bytes: &[u8]) -> Result { - todo!() -} diff --git a/modules/src/lib.rs b/modules/src/lib.rs index f42f0519c9..db92fc25ce 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -18,12 +18,14 @@ //! - ICS 23: Vector Commitment Scheme //! - ICS 24: Host Requirements +pub mod error; pub mod ics02_client; pub mod ics07_tendermint; pub mod ics23_commitment; pub mod ics24_host; pub mod keys; pub mod path; +pub mod query; /// Height of a block, same as in `tendermint` crate pub type Height = tendermint::lite::Height; diff --git a/modules/src/query.rs b/modules/src/query.rs new file mode 100644 index 0000000000..14ad7265a0 --- /dev/null +++ b/modules/src/query.rs @@ -0,0 +1,35 @@ +use tendermint::abci; +use tendermint::rpc::endpoint::abci_query::AbciQuery; + +use crate::error; +use crate::Height; + +/// The type of IBC response sent back for a given IBC `Query`. +pub trait IbcResponse: Sized { + /// The type of the raw response returned by the interface used to query the chain + /// + /// TODO: Uncomment once we abstract over the IBC client + // type RawType; + + /// Build a response of this type from the initial `query` and the IBC `response`. + /// + /// TODO: Replace `AbciQuery` with `Self::RawType` + fn from_abci_response(query: Query, response: AbciQuery) -> Result; +} + +/// Defines an IBC query +pub trait IbcQuery: Sized { + type Response: IbcResponse; + + fn path(&self) -> abci::Path; + fn height(&self) -> Height; + fn prove(&self) -> bool; + fn data(&self) -> Vec; + + /// Build a `Response` from a raw `AbciQuery` response + /// + /// TODO: Replace `AbciQuery` with `>::RawType` + fn build_response(self, response: AbciQuery) -> Result { + Self::Response::from_abci_response(self, response) + } +} diff --git a/relayer/cli/src/commands/query.rs b/relayer/cli/src/commands/query.rs index 5ecb5cf5d1..af19e2d9de 100644 --- a/relayer/cli/src/commands/query.rs +++ b/relayer/cli/src/commands/query.rs @@ -2,7 +2,7 @@ use abscissa_core::{Command, Options, Runnable}; -mod query_client; +mod client; /// `query` subcommand #[derive(Command, Debug, Options, Runnable)] @@ -16,8 +16,8 @@ pub enum QueryCmd { pub enum QueryClientCmds { /// The `query client` subcommand #[options(help = "query client state")] - State(query_client::QueryClientStateCmd), + State(client::QueryClientStateCmd), #[options(help = "query client consensus")] - Consensus(query_client::QueryClientConsensusCmd), + Consensus(client::QueryClientConsensusCmd), } diff --git a/relayer/cli/src/commands/query/query_client.rs b/relayer/cli/src/commands/query/client.rs similarity index 98% rename from relayer/cli/src/commands/query/query_client.rs rename to relayer/cli/src/commands/query/client.rs index ad16087b96..eb61e84378 100644 --- a/relayer/cli/src/commands/query/query_client.rs +++ b/relayer/cli/src/commands/query/client.rs @@ -2,8 +2,7 @@ use crate::prelude::*; use abscissa_core::{Command, Options, Runnable}; use relayer::config::{ChainConfig, Config}; -use relayer::query::client_consensus_state::query_client_consensus_state; -use relayer::query::client_state::query_client_full_state; +use relayer::query::client::{query_client_consensus_state, query_client_full_state}; use relayer_modules::ics24_host::client::ClientId; diff --git a/relayer/relay/src/query.rs b/relayer/relay/src/query.rs index 16fd19236b..4df375e684 100644 --- a/relayer/relay/src/query.rs +++ b/relayer/relay/src/query.rs @@ -1,43 +1,10 @@ use tendermint::abci; -use tendermint::rpc::endpoint::abci_query::AbciQuery; - -use relayer_modules::Height; use crate::chain::Chain; -use crate::error; - -pub mod client_consensus_state; -pub mod client_state; - -/// The type of IBC response sent back for a given IBC `Query`. -pub trait IbcResponse: Sized { - /// The type of the raw response returned by the interface used to query the chain - /// - /// TODO: Uncomment once we abstract over the IBC client - // type RawType; - - /// Build a response of this type from the initial `query` and the IBC `response`. - /// - /// TODO: Replace `AbciQuery` with `Self::RawType` - fn from_abci_response(query: Query, response: AbciQuery) -> Result; -} +use relayer_modules::error; +use relayer_modules::query::IbcQuery; -/// Defines an IBC query -pub trait IbcQuery: Sized { - type Response: IbcResponse; - - fn path(&self) -> abci::Path; - fn height(&self) -> Height; - fn prove(&self) -> bool; - fn data(&self) -> Vec; - - /// Build a `Response` from a raw `AbciQuery` response - /// - /// TODO: Replace `AbciQuery` with `>::RawType` - fn build_response(self, response: AbciQuery) -> Result { - Self::Response::from_abci_response(self, response) - } -} +pub mod client; /// Perform an IBC `query` on the given `chain`, and return the corresponding IBC response. pub async fn ibc_query(chain: &C, query: Q) -> Result diff --git a/relayer/relay/src/query/client.rs b/relayer/relay/src/query/client.rs new file mode 100644 index 0000000000..438d0bdf3c --- /dev/null +++ b/relayer/relay/src/query/client.rs @@ -0,0 +1,36 @@ +use relayer_modules::ics24_host::client::ClientId; +use relayer_modules::Height; + +use super::ibc_query; +use crate::chain::Chain; +use relayer_modules::ics02_client::query::{ClientFullStateResponse, QueryClientFullState}; +use relayer_modules::ics02_client::query::{ConsensusStateResponse, QueryClientConsensusState}; + +use relayer_modules::error; + +pub async fn query_client_full_state( + chain: &C, + chain_height: Height, + client_id: ClientId, + prove: bool, +) -> Result, error::Error> +where + C: Chain, +{ + let query = QueryClientFullState::new(chain_height, client_id, prove); + ibc_query(chain, query).await +} + +pub async fn query_client_consensus_state( + chain: &C, + chain_height: Height, + client_id: ClientId, + consensus_height: Height, + prove: bool, +) -> Result, error::Error> +where + C: Chain, +{ + let query = QueryClientConsensusState::new(chain_height, client_id, consensus_height, prove); + ibc_query(chain, query).await +} diff --git a/relayer/relay/src/query/client_state.rs b/relayer/relay/src/query/client_state.rs deleted file mode 100644 index f0f3d4b5af..0000000000 --- a/relayer/relay/src/query/client_state.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::marker::PhantomData; -use tendermint::rpc::endpoint::abci_query::AbciQuery; - -use tendermint::abci; - -use relayer_modules::ics02_client::state::ClientState; -use relayer_modules::ics23_commitment::{CommitmentPath, CommitmentProof}; -use relayer_modules::ics24_host::client::ClientId; -use relayer_modules::path::{ClientStatePath, Path}; -use relayer_modules::Height; - -use super::{ibc_query, IbcQuery, IbcResponse}; -use crate::chain::Chain; -use crate::error; - -pub struct QueryClientFullState { - pub chain_height: Height, - pub client_id: ClientId, - pub client_state_path: ClientStatePath, - pub prove: bool, - marker: PhantomData, -} - -impl QueryClientFullState { - pub fn new(chain_height: Height, client_id: ClientId, prove: bool) -> Self { - Self { - chain_height, - client_id: client_id.clone(), - client_state_path: ClientStatePath::new(client_id), - prove, - marker: PhantomData, - } - } -} - -impl IbcQuery for QueryClientFullState -where - CLS: ClientState, -{ - type Response = ClientFullStateResponse; - - fn path(&self) -> abci::Path { - "/store/ibc/key".parse().unwrap() - } - - fn height(&self) -> Height { - self.chain_height - } - - fn prove(&self) -> bool { - self.prove - } - - fn data(&self) -> Vec { - self.client_state_path.to_key().into() - } -} - -pub struct ClientFullStateResponse { - pub client_state: CLS, - pub proof: Option, - pub proof_path: CommitmentPath, - pub proof_height: Height, -} - -impl ClientFullStateResponse { - pub fn new( - client_id: ClientId, - client_state: CLS, - abci_proof: Option, - proof_height: Height, - ) -> Self { - let proof_path = CommitmentPath::from_path(ClientStatePath::new(client_id)); - - ClientFullStateResponse { - client_state, - proof: abci_proof, - proof_path, - proof_height, - } - } -} - -impl IbcResponse> for ClientFullStateResponse -where - CLS: ClientState, -{ - fn from_abci_response( - query: QueryClientFullState, - response: AbciQuery, - ) -> Result { - match (response.value, &response.proof) { - (Some(value), _) => { - let client_state = amino_unmarshal_binary_length_prefixed(&value)?; - - Ok(ClientFullStateResponse::new( - query.client_id, - client_state, - response.proof, - response.height.into(), - )) - } - (None, _) => Err(error::Kind::Rpc.context("Bad response").into()), - } - } -} - -pub async fn query_client_full_state( - chain: &C, - chain_height: Height, - client_id: ClientId, - prove: bool, -) -> Result, error::Error> -where - C: Chain, -{ - let query = QueryClientFullState::new(chain_height, client_id, prove); - ibc_query(chain, query).await -} - -fn amino_unmarshal_binary_length_prefixed(_bytes: &[u8]) -> Result { - todo!() -} From 11131dda31df788474b2103c09de7a336a2ac432 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Fri, 17 Apr 2020 12:53:55 +0200 Subject: [PATCH 15/63] Getting the ball rolling w/ CH TLA+ (L3 spec) --- verification/spec/connection-handshake/ch.tla | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 verification/spec/connection-handshake/ch.tla diff --git a/verification/spec/connection-handshake/ch.tla b/verification/spec/connection-handshake/ch.tla new file mode 100644 index 0000000000..996f201f7c --- /dev/null +++ b/verification/spec/connection-handshake/ch.tla @@ -0,0 +1,155 @@ +--------------------------------- MODULE CH --------------------------------- + + +\* ----- ASSUMPTIONS ----- +\* To be refined and reconsidered as this module evolves: +\* - there is only 1 relayer and it is correct. +\* - there are only 2 parties and they are both correct. +\* - Datagrams are simpler (have less fields) than defined in ICS 033. +\* - Parties (i.e., each chain) have a fixed, predefined height. +\* - Relayer is stateless: has no clients, doesn't create proofs. + + +VARIABLES + partyState, \* For each party (A & B), we capture their state. This effectively captures the state of the connection. + pendingDatagrams \* Incoming messages to each party; cf. ibc-rs#55 +\* relayerState, \* Relayer is stateless for the moment. + + +NullParty == "none" +Parties == {"chainA", "chainB"} + +NullConnectionID == "none" +ConnectionIDs == {"connAtoB", "connBtoA"} + +ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} + +Datagrams == + [type : {"ConnOpenInit"}, connectionID : ConnectionIDs, counterpartyConnectionID : ConnectionIDs ] + \union + [type : {"ConnOpenTry"}, desiredConnectionID : ConnectionIDs, counterpartyConnectionID : ConnectionIDs ] + \union + [type : {"ConnOpenAck"}, connectionID : ConnectionIDs ] + \union + [type : {"ConnOpenConfirm"}, connectionID : ConnectionIDs ] + \union + [type : {"NullMsg"} ] \* Unclear if we're going to need this. + + +\* Helper procedures + +\* Would be ideal to import the following two functions, cf. ibc-rs#55 (file: ConnectionHandlers.tla) +getConnectionID(p) == + IF p = "chainA" + THEN "connAtoB" + ELSE IF p = "chainB" + THEN "connBtoA" + ELSE NullConnectionID + +\* get the connection ID of the connection end at chainID's counterparty chain +getCounterpartyConnectionID(p) == + IF p = "chainA" + THEN "connBtoA" + ELSE IF p = "chainB" + THEN "connAtoB" + ELSE NullConnectionID + +getCounterparty(p) == + IF p = "chainA" + THEN "chainB" + ELSE IF p = "chainB" + THEN "chainA" + ELSE NullParty + +isPartyInState(p, state) == + partyState[p] = state + + +\* +\* Helper procedures for constructing datagrams +getInitDatagram(p) == + [ type |-> "ConnOpenInit", + connectionID |-> getConnectionID(p), + counterpartyConnectionID |-> getCounterpartyConnectionID(p) ] + +getTryDatagram(p) == + [ type |-> "ConnOpenTry", + desiredConnectionID |-> getConnectionID(p), + counterpartyConnectionID |-> getCounterpartyConnectionID(p) ] + + +\* +\* Relayer state transitions + +RelayerSendInit(targetParty) == + \* TODO: handle the case where party was already INIT! + /\ \A p \in Parties : isPartyInState(p, "UNINIT") \* Both parties have to be UNINIT + /\ pendingDatagrams' = [pendingDatagrams EXCEPT + ![targetParty] = pendingDatagrams[targetParty] \union { getInitDatagram(targetParty) } + ] + /\ UNCHANGED <> + + +RelayerSendTry(targetParty) == + /\ isPartyInState(targetParty, "UNINIT") + /\ isPartyInState(getCounterparty(targetParty), "INIT") + /\ pendingDatagrams' = [pendingDatagrams EXCEPT + ![targetParty] = pendingDatagrams[targetParty] \union { getTryDatagram(targetParty) } + ] + /\ UNCHANGED <> + +RelayerStep == + \E targetParty \in Parties : + RelayerSendInit(targetParty) + \/ RelayerSendTry(targetParty) + \*\/ RelayerSendAck \/ RelayerSendConfirm /\ RelayerSendNoMsg + + + +\* +\* Party state transitions + +PartyHandleInit(party) == + /\ isPartyInState(party, "UNINIT") \* TODO: allow this handler to be replayed? + /\ isPartyInState(getCounterparty(party), "UNINIT") \* TODO: can a party read the state of the other party? + /\ getInitDatagram(party) \in pendingDatagrams[party] + /\ partyState' = "INIT" + /\ pendingDatagrams' = [pendingDatagrams EXCEPT \* inspired from ibc-rs#55 + ![party] = {} \* TODO: empty or just \minus the datagram?? + ] + + +PartyHandleTry(party) == + /\ isPartyInState(party, "UNINIT") \* TODO: same as for 'PartyHandleInit'. + /\ isPartyInState(getCounterparty(party), "INIT") + /\ getTryDatagram(party) \in pendingDatagrams[party] + /\ partyState' = "INIT" + /\ pendingDatagrams' = [pendingDatagrams EXCEPT + ![party] = {} + ] + +PartyStep(p) == + \E party \in Parties : + PartyHandleInit(party) + \/ PartyHandleTry(party) + + +\* The initial predicate defining the CH protocol. +Init == + \* Associate to each party & relayer an initial state. + /\ partyState = [ p \in Parties |-> "UNINIT" ] + /\ pendingDatagrams = {} + +Next == + \/ RelayerStep + \/ \E p \in Parties : PartyStep(p) + +Spec == + Init /\ [][Next]_<> + + +============================================================================= +\* Modification History +\* Last modified Fri Apr 17 12:47:34 CEST 2020 by adi +\* Created Fri Apr 17 10:28:22 CEST 2020 by adi + From 51ae5c0549d29e2bef965983becc12206bc7d9c0 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Mon, 20 Apr 2020 11:10:19 +0200 Subject: [PATCH 16/63] Added invariant spec & fixed partyState bugs --- verification/spec/connection-handshake/ch.tla | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/verification/spec/connection-handshake/ch.tla b/verification/spec/connection-handshake/ch.tla index 996f201f7c..3b1cae98a2 100644 --- a/verification/spec/connection-handshake/ch.tla +++ b/verification/spec/connection-handshake/ch.tla @@ -1,5 +1,6 @@ --------------------------------- MODULE CH --------------------------------- +EXTENDS Naturals, FiniteSets \* ----- ASSUMPTIONS ----- \* To be refined and reconsidered as this module evolves: @@ -13,7 +14,7 @@ VARIABLES partyState, \* For each party (A & B), we capture their state. This effectively captures the state of the connection. pendingDatagrams \* Incoming messages to each party; cf. ibc-rs#55 -\* relayerState, \* Relayer is stateless for the moment. +\* relayerState, \* Relayer is stateless for the moment. NullParty == "none" @@ -31,8 +32,8 @@ Datagrams == \union [type : {"ConnOpenAck"}, connectionID : ConnectionIDs ] \union - [type : {"ConnOpenConfirm"}, connectionID : ConnectionIDs ] - \union + [type : {"ConnOpenConfirm"}, connectionID : ConnectionIDs ] + \union [type : {"NullMsg"} ] \* Unclear if we're going to need this. @@ -44,7 +45,7 @@ getConnectionID(p) == THEN "connAtoB" ELSE IF p = "chainB" THEN "connBtoA" - ELSE NullConnectionID + ELSE NullConnectionID \* get the connection ID of the connection end at chainID's counterparty chain getCounterpartyConnectionID(p) == @@ -53,7 +54,7 @@ getCounterpartyConnectionID(p) == ELSE IF p = "chainB" THEN "connAtoB" ELSE NullConnectionID - + getCounterparty(p) == IF p = "chainA" THEN "chainB" @@ -64,11 +65,11 @@ getCounterparty(p) == isPartyInState(p, state) == partyState[p] = state - -\* + +\* \* Helper procedures for constructing datagrams getInitDatagram(p) == - [ type |-> "ConnOpenInit", + [ type |-> "ConnOpenInit", connectionID |-> getConnectionID(p), counterpartyConnectionID |-> getCounterpartyConnectionID(p) ] @@ -80,28 +81,30 @@ getTryDatagram(p) == \* \* Relayer state transitions - + RelayerSendInit(targetParty) == - \* TODO: handle the case where party was already INIT! + \* TODO: handle the case where party was already INIT! /\ \A p \in Parties : isPartyInState(p, "UNINIT") \* Both parties have to be UNINIT - /\ pendingDatagrams' = [pendingDatagrams EXCEPT - ![targetParty] = pendingDatagrams[targetParty] \union { getInitDatagram(targetParty) } + /\ pendingDatagrams' = [ + pendingDatagrams EXCEPT + ![targetParty] = pendingDatagrams[targetParty] \union { getInitDatagram(targetParty) } ] /\ UNCHANGED <> - +\* RelayerSendTry(targetParty) == /\ isPartyInState(targetParty, "UNINIT") /\ isPartyInState(getCounterparty(targetParty), "INIT") - /\ pendingDatagrams' = [pendingDatagrams EXCEPT - ![targetParty] = pendingDatagrams[targetParty] \union { getTryDatagram(targetParty) } + /\ pendingDatagrams' = [ + pendingDatagrams EXCEPT + ![targetParty] = pendingDatagrams[targetParty] \union { getTryDatagram(targetParty) } ] /\ UNCHANGED <> RelayerStep == - \E targetParty \in Parties : + \E targetParty \in Parties : RelayerSendInit(targetParty) - \/ RelayerSendTry(targetParty) + \/ RelayerSendTry(targetParty) \*\/ RelayerSendAck \/ RelayerSendConfirm /\ RelayerSendNoMsg @@ -109,27 +112,36 @@ RelayerStep == \* \* Party state transitions +\* TODO: allow this handler to be replayed? PartyHandleInit(party) == - /\ isPartyInState(party, "UNINIT") \* TODO: allow this handler to be replayed? + /\ isPartyInState(party, "UNINIT") /\ isPartyInState(getCounterparty(party), "UNINIT") \* TODO: can a party read the state of the other party? /\ getInitDatagram(party) \in pendingDatagrams[party] - /\ partyState' = "INIT" - /\ pendingDatagrams' = [pendingDatagrams EXCEPT \* inspired from ibc-rs#55 + /\ partyState' = [ + partyState EXCEPT + ![party] = "INIT" + ] + /\ pendingDatagrams' = [ + pendingDatagrams EXCEPT \* inspired from ibc-rs#55 ![party] = {} \* TODO: empty or just \minus the datagram?? ] +\* TODO: same as for 'PartyHandleInit'. PartyHandleTry(party) == - /\ isPartyInState(party, "UNINIT") \* TODO: same as for 'PartyHandleInit'. + /\ isPartyInState(party, "UNINIT") /\ isPartyInState(getCounterparty(party), "INIT") /\ getTryDatagram(party) \in pendingDatagrams[party] - /\ partyState' = "INIT" + /\ partyState' = [ + partyState EXCEPT + ![party] = "TRYOPEN" + ] /\ pendingDatagrams' = [pendingDatagrams EXCEPT ![party] = {} ] -PartyStep(p) == - \E party \in Parties : +PartyStep == + \E party \in Parties : PartyHandleInit(party) \/ PartyHandleTry(party) @@ -138,18 +150,24 @@ PartyStep(p) == Init == \* Associate to each party & relayer an initial state. /\ partyState = [ p \in Parties |-> "UNINIT" ] - /\ pendingDatagrams = {} - -Next == + /\ pendingDatagrams = [ p \in Parties |-> {} ] + +Next == \/ RelayerStep - \/ \E p \in Parties : PartyStep(p) + \/ PartyStep Spec == Init /\ [][Next]_<> +TypeInvariant == + /\ partyState \in [ Parties -> { "UNINIT", "INIT", "TRYOPEN", "OPEN" } ] + /\ pendingDatagrams \subseteq Datagrams + +THEOREM Spec => []TypeInvariant + + ============================================================================= \* Modification History -\* Last modified Fri Apr 17 12:47:34 CEST 2020 by adi +\* Last modified Mon Apr 20 11:08:32 CEST 2020 by adi \* Created Fri Apr 17 10:28:22 CEST 2020 by adi - From c2a8cee4565f9d8357db27c7b29567b2e3fd96bb Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 21 Apr 2020 19:18:53 +0200 Subject: [PATCH 17/63] After discussions w/ A & Z --- verification/spec/connection-handshake/ch.tla | 263 +++++++++--------- 1 file changed, 139 insertions(+), 124 deletions(-) diff --git a/verification/spec/connection-handshake/ch.tla b/verification/spec/connection-handshake/ch.tla index 3b1cae98a2..9e1c4b6923 100644 --- a/verification/spec/connection-handshake/ch.tla +++ b/verification/spec/connection-handshake/ch.tla @@ -1,173 +1,188 @@ --------------------------------- MODULE CH --------------------------------- -EXTENDS Naturals, FiniteSets -\* ----- ASSUMPTIONS ----- -\* To be refined and reconsidered as this module evolves: -\* - there is only 1 relayer and it is correct. -\* - there are only 2 parties and they are both correct. -\* - Datagrams are simpler (have less fields) than defined in ICS 033. -\* - Parties (i.e., each chain) have a fixed, predefined height. -\* - Relayer is stateless: has no clients, doesn't create proofs. +VARIABLES + parties, \* The state of each party. + inboundDatagrams \* For each party, a buffer with any incoming datagrams. -VARIABLES - partyState, \* For each party (A & B), we capture their state. This effectively captures the state of the connection. - pendingDatagrams \* Incoming messages to each party; cf. ibc-rs#55 -\* relayerState, \* Relayer is stateless for the moment. +Parties == {"A", "B"} +ConnectionStates == { "UNINIT" } -NullParty == "none" -Parties == {"chainA", "chainB"} +Null == "none" -NullConnectionID == "none" ConnectionIDs == {"connAtoB", "connBtoA"} -ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} -Datagrams == - [type : {"ConnOpenInit"}, connectionID : ConnectionIDs, counterpartyConnectionID : ConnectionIDs ] - \union - [type : {"ConnOpenTry"}, desiredConnectionID : ConnectionIDs, counterpartyConnectionID : ConnectionIDs ] - \union - [type : {"ConnOpenAck"}, connectionID : ConnectionIDs ] - \union - [type : {"ConnOpenConfirm"}, connectionID : ConnectionIDs ] - \union - [type : {"NullMsg"} ] \* Unclear if we're going to need this. +ClientIDs == { "clientA", "clientB" } + + +(***************************** ConnectionEnds ***************************** + Ref.: https://github.com/informalsystems/ibc-rs/pull/55. + + A set of connection end records. + A connection end record contains the following fields: + + - state -- a string + Stores the current state of this connection end. It has one of the + following values: "UNINIT", "INIT", "TRYOPEN", "OPEN". + + - connectionID -- a connection identifier + Stores the connection identifier of this connection end. + + - counterpartyConnectionID -- a connection identifier + Stores the connection identifier of the counterparty connection end. + + - clientID -- a client identifier + Stores the client identifier associated with this connection end. + + - counterpartyClientID -- a client identifier + Stores the counterparty client identifier associated with this connection end. + ***************************************************************************) +ConnectionEnd == + [ + state : ConnectionStates, + connectionID : ConnectionIDs \union {Null}, + clientID : ClientIDs \union {Null}, + counterpartyConnectionID : ConnectionIDs \union {Null}, + counterpartyClientID : ClientIDs \union {Null} + ] + +(********************************** Helper functions + ***************************************************************************) + +\* Constructs a ConnectionEnd object for the given party in state UNINIT. +getConnectioEnd(targetParty) == + IF targetParty = "A" + THEN + [ state |-> "UNINIT", + connectionID |-> "connAtoB", + clientID |-> "clientA", + counterpartyConnectionID |-> "connBtoA", + counterpartyClientID |-> "clientB" ] + ELSE IF targetParty = "B" + THEN + [ state |-> "UNINIT", + connectionID |-> "connBtoA", + clientID |-> "clientB", + counterpartyConnectionID |-> "connAtoB", + counterpartyClientID |-> "clientA" ] + ELSE Null + + +\* Constructs a 'Init' Datagram. +\* Fields are hardcoded for the moment +getInitDatagram(targetParty) == + [ type |-> "ConnOpenInit", + connectionEnd |-> getConnectioEnd(targetParty) ] -\* Helper procedures +isPartyInState(p, state) == + parties[p].connectionEnd.state = state -\* Would be ideal to import the following two functions, cf. ibc-rs#55 (file: ConnectionHandlers.tla) + +\* Would be ideal to import the following function. +\* Cf. ibc-rs#55 (file: ConnectionHandlers.tla) getConnectionID(p) == - IF p = "chainA" + IF p = "A" THEN "connAtoB" - ELSE IF p = "chainB" + ELSE IF p = "B" THEN "connBtoA" - ELSE NullConnectionID - -\* get the connection ID of the connection end at chainID's counterparty chain -getCounterpartyConnectionID(p) == - IF p = "chainA" - THEN "connBtoA" - ELSE IF p = "chainB" - THEN "connAtoB" - ELSE NullConnectionID - -getCounterparty(p) == - IF p = "chainA" - THEN "chainB" - ELSE IF p = "chainB" - THEN "chainA" - ELSE NullParty - -isPartyInState(p, state) == - partyState[p] = state - - -\* -\* Helper procedures for constructing datagrams -getInitDatagram(p) == - [ type |-> "ConnOpenInit", - connectionID |-> getConnectionID(p), - counterpartyConnectionID |-> getCounterpartyConnectionID(p) ] - -getTryDatagram(p) == - [ type |-> "ConnOpenTry", - desiredConnectionID |-> getConnectionID(p), - counterpartyConnectionID |-> getCounterpartyConnectionID(p) ] + ELSE Null -\* -\* Relayer state transitions +(********************************** Environment functions + ***************************************************************************) -RelayerSendInit(targetParty) == - \* TODO: handle the case where party was already INIT! - /\ \A p \in Parties : isPartyInState(p, "UNINIT") \* Both parties have to be UNINIT - /\ pendingDatagrams' = [ - pendingDatagrams EXCEPT - ![targetParty] = pendingDatagrams[targetParty] \union { getInitDatagram(targetParty) } + +\* The environment generates the ConnOpenInit datagram, sending it to 'party' +GenerateConnOpenInit(targetParty) == + LET initDatagram == getInitDatagram(targetParty) + IN inboundDatagrams' = [ + inboundDatagrams EXCEPT + ![targetParty] = inboundDatagrams[targetParty] \union { initDatagram } ] - /\ UNCHANGED <> - -\* -RelayerSendTry(targetParty) == - /\ isPartyInState(targetParty, "UNINIT") - /\ isPartyInState(getCounterparty(targetParty), "INIT") - /\ pendingDatagrams' = [ - pendingDatagrams EXCEPT - ![targetParty] = pendingDatagrams[targetParty] \union { getTryDatagram(targetParty) } - ] - /\ UNCHANGED <> + /\ UNCHANGED <> + -RelayerStep == +EnvStep == \E targetParty \in Parties : - RelayerSendInit(targetParty) - \/ RelayerSendTry(targetParty) - \*\/ RelayerSendAck \/ RelayerSendConfirm /\ RelayerSendNoMsg + GenerateConnOpenInit(targetParty) +(**************************** Party functions + ***************************************************************************) -\* -\* Party state transitions +ConnectionEndInit == + [state |-> "UNINIT", + connectionID |-> Null, + clientID |-> Null, + counterpartyConnectionID |-> Null, + counterpartyClientID |-> Null ] -\* TODO: allow this handler to be replayed? -PartyHandleInit(party) == - /\ isPartyInState(party, "UNINIT") - /\ isPartyInState(getCounterparty(party), "UNINIT") \* TODO: can a party read the state of the other party? - /\ getInitDatagram(party) \in pendingDatagrams[party] - /\ partyState' = [ - partyState EXCEPT - ![party] = "INIT" - ] - /\ pendingDatagrams' = [ - pendingDatagrams EXCEPT \* inspired from ibc-rs#55 - ![party] = {} \* TODO: empty or just \minus the datagram?? - ] +(** + * Connection Handshake Module handlers + **) -\* TODO: same as for 'PartyHandleInit'. -PartyHandleTry(party) == +CHModuleHandleInit(party) == /\ isPartyInState(party, "UNINIT") - /\ isPartyInState(getCounterparty(party), "INIT") - /\ getTryDatagram(party) \in pendingDatagrams[party] - /\ partyState' = [ - partyState EXCEPT - ![party] = "TRYOPEN" - ] - /\ pendingDatagrams' = [pendingDatagrams EXCEPT - ![party] = {} - ] + /\ LET connOpenInitDgrs == {dgr \in inboundDatagrams[party] : + /\ dgr.type = "ConnOpenInit" + /\ dgr.connectionID = getConnectionID(party)} + IN IF connOpenInitDgrs /= {} + THEN LET connOpenInitDgr == CHOOSE dgr \in connOpenInitDgrs : TRUE + IN LET connOpenInitConnectionEnd == [ + state |-> "INIT", + connectionID |-> connOpenInitDgr.connectionID, + clientID |-> connOpenInitDgr.clientID, + counterpartyConnectionID |-> connOpenInitDgr.counterpartyConnectionID, + counterpartyClientID |-> connOpenInitDgr.counterpartyClientID ] + IN parties' = [ parties EXCEPT + ![party] = [ connectionEnd |-> connOpenInitConnectionEnd ] + ] + /\ inboundDatagrams' = [ + inboundDatagrams EXCEPT + ![party] = {} ] + ELSE Null \* TODO: no return value? unclear. + + +PartyInit == + [ connectionEnd |-> ConnectionEndInit ] PartyStep == \E party \in Parties : - PartyHandleInit(party) - \/ PartyHandleTry(party) + \/ CHModuleHandleInit(party) + +(**************************** Main spec + ***************************************************************************) -\* The initial predicate defining the CH protocol. -Init == - \* Associate to each party & relayer an initial state. - /\ partyState = [ p \in Parties |-> "UNINIT" ] - /\ pendingDatagrams = [ p \in Parties |-> {} ] -Next == - \/ RelayerStep +Init == + \* Associate to each party an initial state. + /\ parties = [ p \in Parties |-> PartyInit ] + /\ inboundDatagrams = [ p \in Parties |-> {} ] + +Next == \/ PartyStep + \/ EnvStep Spec == - Init /\ [][Next]_<> + Init /\ [][Next]_<> TypeInvariant == - /\ partyState \in [ Parties -> { "UNINIT", "INIT", "TRYOPEN", "OPEN" } ] - /\ pendingDatagrams \subseteq Datagrams + /\ parties \in [ Parties -> { "UNINIT", "INIT", "TRYOPEN", "OPEN" } ] +\* /\ inboundDatagrams \subseteq Datagrams +\* Model check it! THEOREM Spec => []TypeInvariant ============================================================================= \* Modification History -\* Last modified Mon Apr 20 11:08:32 CEST 2020 by adi +\* Last modified Tue Apr 21 19:17:35 CEST 2020 by adi \* Created Fri Apr 17 10:28:22 CEST 2020 by adi + From bb4808f18a1badd5bf6369beb82e34f4435ef2c3 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 22 Apr 2020 16:21:55 +0200 Subject: [PATCH 18/63] major refactoring ht/ Anca and comments from Zarko --- verification/spec/connection-handshake/ch.tla | 286 ++++++++++-------- 1 file changed, 155 insertions(+), 131 deletions(-) diff --git a/verification/spec/connection-handshake/ch.tla b/verification/spec/connection-handshake/ch.tla index 9e1c4b6923..34d3d03c03 100644 --- a/verification/spec/connection-handshake/ch.tla +++ b/verification/spec/connection-handshake/ch.tla @@ -1,181 +1,204 @@ --------------------------------- MODULE CH --------------------------------- +EXTENDS Naturals, FiniteSets -VARIABLES - parties, \* The state of each party. - inboundDatagrams \* For each party, a buffer with any incoming datagrams. +CONSTANT MaxHeight \* Maximum height of all the chains in the system. -Parties == {"A", "B"} -ConnectionStates == { "UNINIT" } +VARIABLES + turn, \* Keep track of who takes a step: either connection module or environment. + chain, \* The state of this (local) chain. + inMsg, \* A buffer holding any message incoming to the chain. + outMsg \* A buffer holding any message outbound from the chain. -Null == "none" -ConnectionIDs == {"connAtoB", "connBtoA"} +vars == <> +Heights == 1..MaxHeight +ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} + +ConnectionIDs == {"connAtoB", "connBtoA"} ClientIDs == { "clientA", "clientB" } +Null == "none" +noMsg == [ type |-> "none" ] +noErr == "errNone" + (***************************** ConnectionEnds ***************************** - Ref.: https://github.com/informalsystems/ibc-rs/pull/55. - - A set of connection end records. + A set of connection end records. A connection end record contains the following fields: - - - state -- a string - Stores the current state of this connection end. It has one of the + + - state -- a string + Stores the current state of this connection end. It has one of the following values: "UNINIT", "INIT", "TRYOPEN", "OPEN". - + - connectionID -- a connection identifier Stores the connection identifier of this connection end. - + - counterpartyConnectionID -- a connection identifier Stores the connection identifier of the counterparty connection end. - + - clientID -- a client identifier - Stores the client identifier associated with this connection end. - + Stores the client identifier associated with this connection end. + - counterpartyClientID -- a client identifier Stores the counterparty client identifier associated with this connection end. ***************************************************************************) -ConnectionEnd == +ConnectionEnds == [ state : ConnectionStates, connectionID : ConnectionIDs \union {Null}, clientID : ClientIDs \union {Null}, counterpartyConnectionID : ConnectionIDs \union {Null}, counterpartyClientID : ClientIDs \union {Null} - ] + ] -(********************************** Helper functions - ***************************************************************************) +(******************************** Datagrams ******************************** +Specify the connection-handshake specific datagrams. +TODO: Unclear if we should insist on calling these 'datagrams' or more +generally just 'messages' (@Zarko). -\* Constructs a ConnectionEnd object for the given party in state UNINIT. -getConnectioEnd(targetParty) == - IF targetParty = "A" - THEN - [ state |-> "UNINIT", - connectionID |-> "connAtoB", - clientID |-> "clientA", - counterpartyConnectionID |-> "connBtoA", - counterpartyClientID |-> "clientB" ] - ELSE IF targetParty = "B" - THEN - [ state |-> "UNINIT", - connectionID |-> "connBtoA", - clientID |-> "clientB", - counterpartyConnectionID |-> "connAtoB", - counterpartyClientID |-> "clientA" ] - ELSE Null - - -\* Constructs a 'Init' Datagram. -\* Fields are hardcoded for the moment -getInitDatagram(targetParty) == - [ type |-> "ConnOpenInit", - connectionEnd |-> getConnectioEnd(targetParty) ] - - -isPartyInState(p, state) == - parties[p].connectionEnd.state = state - - -\* Would be ideal to import the following function. -\* Cf. ibc-rs#55 (file: ConnectionHandlers.tla) -getConnectionID(p) == - IF p = "A" - THEN "connAtoB" - ELSE IF p = "B" - THEN "connBtoA" - ELSE Null - - -(********************************** Environment functions +TODO: @Zarko: In my view this abstraction should be called ConnectionEnd +and what is currently called ConnectionEnd should be Connection. ***************************************************************************) - - -\* The environment generates the ConnOpenInit datagram, sending it to 'party' -GenerateConnOpenInit(targetParty) == - LET initDatagram == getInitDatagram(targetParty) - IN inboundDatagrams' = [ - inboundDatagrams EXCEPT - ![targetParty] = inboundDatagrams[targetParty] \union { initDatagram } - ] - /\ UNCHANGED <> - - -EnvStep == - \E targetParty \in Parties : - GenerateConnOpenInit(targetParty) - - -(**************************** Party functions +ConnectionDatagrams == + [type : {"ConnOpenInit"}, connectionID : ConnectionIDs, clientID : ClientIDs, + counterpartyConnectionID : ConnectionIDs, counterpartyClientID : ClientIDs] + \union + [type : {"ConnOpenTry"}, desiredConnectionID : ConnectionIDs, + counterpartyConnectionID : ConnectionIDs, counterpartyClientID : ClientIDs, + clientID : ClientIDs, proofHeight : Heights, consensusHeight : Heights] + + + +(*************************************************************************** +Connection datagram handlers +***************************************************************************) + +\* Handle "ConnOpenInit" datagrams +connectionOpenInit(ch, datagram) == + IF ch.connectionEnd.state = "UNINIT" THEN + LET connOpenInitConnectionEnd == [ + state |-> "INIT", + connectionID |-> datagram.connectionID, + clientID |-> datagram.clientID, + counterpartyConnectionID |-> datagram.counterpartyConnectionID, + counterpartyClientID |-> datagram.counterpartyClientID] + IN + [ chain |-> [ch EXCEPT !.connectionEnd = connOpenInitConnectionEnd], + msg |-> [ + type |-> "ConnOpenTry", + connectionID |-> connOpenInitConnectionEnd.counterpartyConnectionID, + clientID |-> connOpenInitConnectionEnd.counterpartyClientID, + counterpartyConnectionID |-> connOpenInitConnectionEnd.connectionID, + counterpartyClientID |-> connOpenInitConnectionEnd.clientID ] ] + \* otherwise, do not update the chain + ELSE [ chain |-> ch, + msg |-> noMsg ] + +handleMsgConnectionOpenInit == + /\ inMsg.type = "ConnOpenInit" + /\ LET res == connectionOpenInit(chain, inMsg) IN + /\ chain' = res.chain + /\ outMsg' = res.msg + + + +(*************************************************************************** + Chain actions ***************************************************************************) -ConnectionEndInit == +\* Advance the height of the chain until MaxHeight is reached +advanceChain == + /\ chain.height < MaxHeight + /\ chain' = [chain EXCEPT + !.height = chain.height + 1 + ] + /\ UNCHANGED outMsg + +(*** + * Initial values for the chain. + **) +InitConnectionEnd == [state |-> "UNINIT", connectionID |-> Null, clientID |-> Null, counterpartyConnectionID |-> Null, - counterpartyClientID |-> Null ] - - -(** - * Connection Handshake Module handlers - **) - -CHModuleHandleInit(party) == - /\ isPartyInState(party, "UNINIT") - /\ LET connOpenInitDgrs == {dgr \in inboundDatagrams[party] : - /\ dgr.type = "ConnOpenInit" - /\ dgr.connectionID = getConnectionID(party)} - IN IF connOpenInitDgrs /= {} - THEN LET connOpenInitDgr == CHOOSE dgr \in connOpenInitDgrs : TRUE - IN LET connOpenInitConnectionEnd == [ - state |-> "INIT", - connectionID |-> connOpenInitDgr.connectionID, - clientID |-> connOpenInitDgr.clientID, - counterpartyConnectionID |-> connOpenInitDgr.counterpartyConnectionID, - counterpartyClientID |-> connOpenInitDgr.counterpartyClientID ] - IN parties' = [ parties EXCEPT - ![party] = [ connectionEnd |-> connOpenInitConnectionEnd ] - ] - /\ inboundDatagrams' = [ - inboundDatagrams EXCEPT - ![party] = {} ] - ELSE Null \* TODO: no return value? unclear. - - -PartyInit == - [ connectionEnd |-> ConnectionEndInit ] - -PartyStep == - \E party \in Parties : - \/ CHModuleHandleInit(party) - - -(**************************** Main spec + counterpartyClientID |-> Null] + +InitChain == + [height |-> 1, + connectionEnd |-> InitConnectionEnd] + +InitCh == + /\ chain = InitChain + /\ outMsg = noMsg + +NextCh == + \/ advanceChain + \/ handleMsgConnectionOpenInit + +(*************************************************************************** + Environment actions ***************************************************************************) + InitEnv == + /\ inMsg = noMsg + + OnConnInitMsg == + /\ inMsg' \in [type: {"ConnOpenInit"}, + connectionID : ConnectionIDs, + clientID : ClientIDs, + counterpartyConnectionID : ConnectionIDs, + counterpartyClientID : ClientIDs] + + NextEnv == + \/ OnConnInitMsg + + +(****************************************************************************** + Main spec. + The system comprises the connection handshake module & environment. + *****************************************************************************) Init == - \* Associate to each party an initial state. - /\ parties = [ p \in Parties |-> PartyInit ] - /\ inboundDatagrams = [ p \in Parties |-> {} ] - -Next == - \/ PartyStep - \/ EnvStep + turn = "env" \* Initially, the environment takes a turn. + /\ InitEnv + /\ InitCh + +\* Turn-flipping mechanism. +FlipTurn == + turn' = ( + IF turn = "ch" THEN + "env" + ELSE + "ch" + ) + +\* chModule and environment alternate their steps +Next == +/\ FlipTurn +/\ IF turn = "ch" THEN + /\ NextCh + /\ inMsg' = noMsg + ELSE + /\ NextEnv + /\ outMsg' = noMsg + /\ UNCHANGED chain + \* Handle the exceptional case when turn is neither of ch or env? Spec == - Init /\ [][Next]_<> + Init + /\ [][Next]_<> + /\ WF_turn(FlipTurn) TypeInvariant == - /\ parties \in [ Parties -> { "UNINIT", "INIT", "TRYOPEN", "OPEN" } ] -\* /\ inboundDatagrams \subseteq Datagrams + /\ inMsg \in ConnectionDatagrams + \* Model check it! THEOREM Spec => []TypeInvariant @@ -183,6 +206,7 @@ THEOREM Spec => []TypeInvariant ============================================================================= \* Modification History -\* Last modified Tue Apr 21 19:17:35 CEST 2020 by adi +\* Last modified Wed Apr 22 16:19:44 CEST 2020 by adi +\* Wed Apr 22 10:42:14 CEST 2020 improvements & suggestions by anca & zarko \* Created Fri Apr 17 10:28:22 CEST 2020 by adi From e848a84126f99085e5328ca3c7912ab915b9a45d Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 22 Apr 2020 17:48:44 +0200 Subject: [PATCH 19/63] Added abstractions. --- verification/spec/connection-handshake/ch.tla | 207 ++++++++++-------- 1 file changed, 118 insertions(+), 89 deletions(-) diff --git a/verification/spec/connection-handshake/ch.tla b/verification/spec/connection-handshake/ch.tla index 34d3d03c03..b522364d63 100644 --- a/verification/spec/connection-handshake/ch.tla +++ b/verification/spec/connection-handshake/ch.tla @@ -7,13 +7,13 @@ CONSTANT MaxHeight \* Maximum height of all the chains in the system. VARIABLES - turn, \* Keep track of who takes a step: either connection module or environment. - chain, \* The state of this (local) chain. - inMsg, \* A buffer holding any message incoming to the chain. - outMsg \* A buffer holding any message outbound from the chain. + chModule, \* The state of this connection handshake module (chm). + turn, \* Keep track of who takes a step: either chModule or environment. + inMsg, \* A buffer holding any message incoming to the chModule. + outMsg \* A buffer holding any message outbound from the chModule. -vars == <> +vars == <> Heights == 1..MaxHeight @@ -31,113 +31,142 @@ noErr == "errNone" A set of connection end records. A connection end record contains the following fields: - - state -- a string - Stores the current state of this connection end. It has one of the - following values: "UNINIT", "INIT", "TRYOPEN", "OPEN". - - connectionID -- a connection identifier - Stores the connection identifier of this connection end. - - - counterpartyConnectionID -- a connection identifier - Stores the connection identifier of the counterparty connection end. + Stores the connection identifier for this connection end. - clientID -- a client identifier Stores the client identifier associated with this connection end. - - counterpartyClientID -- a client identifier - Stores the counterparty client identifier associated with this connection end. + Optional, add later: + - prefix + ***************************************************************************) ConnectionEnds == [ - state : ConnectionStates, connectionID : ConnectionIDs \union {Null}, - clientID : ClientIDs \union {Null}, - counterpartyConnectionID : ConnectionIDs \union {Null}, - counterpartyClientID : ClientIDs \union {Null} + clientID : ClientIDs \union {Null} + ] + + +(***************************** Connections ***************************** + A set of connection records. + A connection record contains the following fields: + + - localEnd -- a connection end + Stores the connection end for the local chain. + + - remoteEnd -- a connection end + Stores the connection end for the remote chain. + ***************************************************************************) +Connections == + [ + localEnd : ConnectionEnds, + remoteEnd : ConnectionEnds + ] + + +(***************************** ConnectionHandshakeModules ****************** + A set of records defining the CHM. + A CHM record contains the following fields: + + - connectionState -- a string + Stores the current state of this connection. It has one of the + following values: "UNINIT", "INIT", "TRYOPEN", "OPEN". + + - connection -- a connection with two ends + Stores the connection. + + - chainHeight -- a height + Stores the height of the chain running alongside the CHM. + ***************************************************************************) +ConnectionHandshakeModules == + [ + connectionState : ConnectionStates, + connection : Connections, + chainHeight : Heights + \* light client, consensus state, etc.. ] -(******************************** Datagrams ******************************** -Specify the connection-handshake specific datagrams. -TODO: Unclear if we should insist on calling these 'datagrams' or more -generally just 'messages' (@Zarko). -TODO: @Zarko: In my view this abstraction should be called ConnectionEnd -and what is currently called ConnectionEnd should be Connection. + +(******************************** Messages ******************************** +These messages are connection handshake specific. ***************************************************************************) -ConnectionDatagrams == - [type : {"ConnOpenInit"}, connectionID : ConnectionIDs, clientID : ClientIDs, - counterpartyConnectionID : ConnectionIDs, counterpartyClientID : ClientIDs] - \union - [type : {"ConnOpenTry"}, desiredConnectionID : ConnectionIDs, - counterpartyConnectionID : ConnectionIDs, counterpartyClientID : ClientIDs, - clientID : ClientIDs, proofHeight : Heights, consensusHeight : Heights] +ConnectionHandshakeMessages == + [type : {"ConnOpenInit"}, + connection : Connections] +\* \union +\* [type : {"ConnOpenTry"}, +\* connection : Connections, +\* stateProof : Proofs, +\* consensusHeight : Proofs] (*************************************************************************** -Connection datagram handlers + Connection handshake message handlers. ***************************************************************************) -\* Handle "ConnOpenInit" datagrams -connectionOpenInit(ch, datagram) == - IF ch.connectionEnd.state = "UNINIT" THEN - LET connOpenInitConnectionEnd == [ - state |-> "INIT", - connectionID |-> datagram.connectionID, - clientID |-> datagram.clientID, - counterpartyConnectionID |-> datagram.counterpartyConnectionID, - counterpartyClientID |-> datagram.counterpartyClientID] - IN - [ chain |-> [ch EXCEPT !.connectionEnd = connOpenInitConnectionEnd], - msg |-> [ - type |-> "ConnOpenTry", - connectionID |-> connOpenInitConnectionEnd.counterpartyConnectionID, - clientID |-> connOpenInitConnectionEnd.counterpartyClientID, - counterpartyConnectionID |-> connOpenInitConnectionEnd.connectionID, - counterpartyClientID |-> connOpenInitConnectionEnd.clientID ] ] - \* otherwise, do not update the chain - ELSE [ chain |-> ch, - msg |-> noMsg ] +\* The chModule handles a "ConnOpenInit" message. +connectionOpenInit(chm, initMsg) == + \* If we're in the right state. + IF chm.connectionState = "UNINIT" THEN + [initCHM |-> [chm EXCEPT !.connection = initMsg.connection, + !.connectionState = "INIT"], + msg |-> [type |-> "ConnOpenTry", + connection |-> initMsg.connection]] + \* TODO: Proofs go here. + \* Otherwise, do not update the chain + ELSE [initCHM |-> chm, + msg |-> noMsg ] handleMsgConnectionOpenInit == /\ inMsg.type = "ConnOpenInit" - /\ LET res == connectionOpenInit(chain, inMsg) IN - /\ chain' = res.chain + /\ LET res == connectionOpenInit(chModule, inMsg) IN + /\ chModule' = res.initCHM /\ outMsg' = res.msg + /\ UNCHANGED <> (*************************************************************************** - Chain actions + Connection Handshake Module actions. ***************************************************************************) -\* Advance the height of the chain until MaxHeight is reached +\* Advance the height of the chain if MaxHeight is not yet reached. advanceChain == - /\ chain.height < MaxHeight - /\ chain' = [chain EXCEPT - !.height = chain.height + 1 + /\ chModule.chainHeight < MaxHeight + /\ chModule' = [chModule EXCEPT + !.chainHeight = chModule.chainHeight + 1 ] - /\ UNCHANGED outMsg + /\ UNCHANGED <> + (*** - * Initial values for the chain. + * Initial value for a connection end. **) -InitConnectionEnd == - [state |-> "UNINIT", - connectionID |-> Null, - clientID |-> Null, - counterpartyConnectionID |-> Null, - counterpartyClientID |-> Null] - -InitChain == - [height |-> 1, - connectionEnd |-> InitConnectionEnd] - -InitCh == - /\ chain = InitChain +ConnectionEndInitValue == + [connectionID |-> Null, + clientID |-> Null] + + +(*** + * Initial values for a connection. + **) +ConnectionInitValue == + [localEnd |-> ConnectionEndInitValue, + remoteEnd |-> ConnectionEndInitValue] + +CHModuleInitValue == + [chainHeight |-> 1, + connectionState |-> "UNINT", + connection |-> ConnectionInitValue ] + +InitCHModule == + /\ chModule = CHModuleInitValue /\ outMsg = noMsg -NextCh == +NextCHModule == \/ advanceChain \/ handleMsgConnectionOpenInit @@ -150,10 +179,7 @@ NextCh == OnConnInitMsg == /\ inMsg' \in [type: {"ConnOpenInit"}, - connectionID : ConnectionIDs, - clientID : ClientIDs, - counterpartyConnectionID : ConnectionIDs, - counterpartyClientID : ClientIDs] + connection : Connections] NextEnv == \/ OnConnInitMsg @@ -167,28 +193,30 @@ NextCh == Init == turn = "env" \* Initially, the environment takes a turn. /\ InitEnv - /\ InitCh + /\ InitCHModule + \* Turn-flipping mechanism. FlipTurn == turn' = ( - IF turn = "ch" THEN + IF turn = "chm" THEN "env" ELSE - "ch" + "chm" ) + \* chModule and environment alternate their steps Next == /\ FlipTurn -/\ IF turn = "ch" THEN - /\ NextCh +/\ IF turn = "chm" THEN + /\ NextCHModule /\ inMsg' = noMsg ELSE /\ NextEnv /\ outMsg' = noMsg - /\ UNCHANGED chain - \* Handle the exceptional case when turn is neither of ch or env? + /\ UNCHANGED chModule + \* Handle the exceptional case when turn is neither of chm or env? Spec == Init @@ -197,7 +225,7 @@ Spec == TypeInvariant == - /\ inMsg \in ConnectionDatagrams + /\ inMsg \in ConnectionHandshakeMessages \union { noMsg } \* Model check it! @@ -206,7 +234,8 @@ THEOREM Spec => []TypeInvariant ============================================================================= \* Modification History -\* Last modified Wed Apr 22 16:19:44 CEST 2020 by adi +\* Last modified Wed Apr 22 17:43:57 CEST 2020 by adi \* Wed Apr 22 10:42:14 CEST 2020 improvements & suggestions by anca & zarko \* Created Fri Apr 17 10:28:22 CEST 2020 by adi + From bec60a512a1593a11b20fcf34b018b533961b80c Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 23 Apr 2020 11:07:08 +0200 Subject: [PATCH 20/63] Renamed the main file to reflect the CH 'module' within. --- verification/spec/connection-handshake/{ch.tla => chm.tla} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename verification/spec/connection-handshake/{ch.tla => chm.tla} (100%) diff --git a/verification/spec/connection-handshake/ch.tla b/verification/spec/connection-handshake/chm.tla similarity index 100% rename from verification/spec/connection-handshake/ch.tla rename to verification/spec/connection-handshake/chm.tla From a0d7728426a90f22353b725f18aec0b71ffeb637 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 23 Apr 2020 11:12:40 +0200 Subject: [PATCH 21/63] quick fix for the inMsg deadlock --- verification/spec/connection-handshake/chm.tla | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/verification/spec/connection-handshake/chm.tla b/verification/spec/connection-handshake/chm.tla index b522364d63..0ee9e3e745 100644 --- a/verification/spec/connection-handshake/chm.tla +++ b/verification/spec/connection-handshake/chm.tla @@ -1,4 +1,4 @@ ---------------------------------- MODULE CH --------------------------------- +-------------------------------- MODULE CHM -------------------------------- EXTENDS Naturals, FiniteSets @@ -134,7 +134,7 @@ handleMsgConnectionOpenInit == ***************************************************************************) \* Advance the height of the chain if MaxHeight is not yet reached. -advanceChain == +advanceChainHeight == /\ chModule.chainHeight < MaxHeight /\ chModule' = [chModule EXCEPT !.chainHeight = chModule.chainHeight + 1 @@ -167,8 +167,9 @@ InitCHModule == /\ outMsg = noMsg NextCHModule == - \/ advanceChain + \/ advanceChainHeight \/ handleMsgConnectionOpenInit + /\ inMsg' = noMsg \* The chm consumed its inbound message. (*************************************************************************** Environment actions @@ -211,10 +212,9 @@ Next == /\ FlipTurn /\ IF turn = "chm" THEN /\ NextCHModule - /\ inMsg' = noMsg ELSE /\ NextEnv - /\ outMsg' = noMsg + /\ outMsg' = noMsg \* The env. consumed the outbound message. /\ UNCHANGED chModule \* Handle the exceptional case when turn is neither of chm or env? @@ -234,7 +234,7 @@ THEOREM Spec => []TypeInvariant ============================================================================= \* Modification History -\* Last modified Wed Apr 22 17:43:57 CEST 2020 by adi +\* Last modified Thu Apr 23 11:11:24 CEST 2020 by adi \* Wed Apr 22 10:42:14 CEST 2020 improvements & suggestions by anca & zarko \* Created Fri Apr 17 10:28:22 CEST 2020 by adi From f590116d7c8f399f3ef21064e15dcb058be937d8 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 23 Apr 2020 11:23:40 +0200 Subject: [PATCH 22/63] fixed second deadlock with inMsg --- verification/spec/connection-handshake/chm.tla | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/verification/spec/connection-handshake/chm.tla b/verification/spec/connection-handshake/chm.tla index 0ee9e3e745..1d5112be63 100644 --- a/verification/spec/connection-handshake/chm.tla +++ b/verification/spec/connection-handshake/chm.tla @@ -125,9 +125,7 @@ handleMsgConnectionOpenInit == /\ LET res == connectionOpenInit(chModule, inMsg) IN /\ chModule' = res.initCHM /\ outMsg' = res.msg - /\ UNCHANGED <> - - + /\ inMsg' = noMsg \* The chm consumed its inbound message. (*************************************************************************** Connection Handshake Module actions. @@ -137,8 +135,7 @@ handleMsgConnectionOpenInit == advanceChainHeight == /\ chModule.chainHeight < MaxHeight /\ chModule' = [chModule EXCEPT - !.chainHeight = chModule.chainHeight + 1 - ] + !.chainHeight = chModule.chainHeight + 1] /\ UNCHANGED <> @@ -168,8 +165,7 @@ InitCHModule == NextCHModule == \/ advanceChainHeight - \/ handleMsgConnectionOpenInit - /\ inMsg' = noMsg \* The chm consumed its inbound message. + \/ handleMsgConnectionOpenInit (*************************************************************************** Environment actions @@ -234,7 +230,7 @@ THEOREM Spec => []TypeInvariant ============================================================================= \* Modification History -\* Last modified Thu Apr 23 11:11:24 CEST 2020 by adi +\* Last modified Thu Apr 23 11:22:41 CEST 2020 by adi \* Wed Apr 22 10:42:14 CEST 2020 improvements & suggestions by anca & zarko \* Created Fri Apr 17 10:28:22 CEST 2020 by adi From ad267d42dd66496d34198bfcd5f6b5ead3dd64ee Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 28 Apr 2020 11:22:51 +0200 Subject: [PATCH 23/63] Significant progress on modularizing CH TLA spec. --- .../ConnectionHandshakeModule.tla | 172 +++++++++++++ .../spec/connection-handshake/Environment.tla | 108 ++++++++ .../spec/connection-handshake/chm.tla | 237 ------------------ 3 files changed, 280 insertions(+), 237 deletions(-) create mode 100644 verification/spec/connection-handshake/ConnectionHandshakeModule.tla create mode 100644 verification/spec/connection-handshake/Environment.tla delete mode 100644 verification/spec/connection-handshake/chm.tla diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla new file mode 100644 index 0000000000..ab2449d349 --- /dev/null +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -0,0 +1,172 @@ +--------------------- MODULE ConnectionHandshakeModule --------------------- + +EXTENDS Naturals, TLC + +\* Maximum height of the chain where this moduele is running. +CONSTANT MaxHeight + + +VARIABLES + inMsg, \* A buffer holding any message incoming to the chModule. + outMsg, \* A buffer holding any message outbound from the chModule. + store \* The local store of the chain running this chModule. + + + +ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} + +ConnectionIDs == {"connAtoB", "connBtoA"} +nullConnectionID == "null" + +ClientIDs == { "clientA", "clientB" } +nullClientID == "null" + +ConnectionEnds == + [ + connectionID : ConnectionIDs, + clientID : ClientIDs + \* commitmentPrefix add later + ] + +ConnectionParameters == + [ + localEnd : ConnectionEnds, + remoteEnd : ConnectionEnds + ] + +Connections == + [ + parameters : ConnectionParameters, + state : ConnectionStates + \* any other fields that are specific for a connection + ] + +nullConnection == "null" + + +(******************************** Messages ******************************** +These messages are connection handshake specific. + ***************************************************************************) +ConnectionHandshakeMessages == + [type : {"ConnOpenInit"}, + parameters : ConnectionParameters] + \union + [type : {"ConnOpenTry"}, + parameters : ConnectionParameters +\* stateProof : Proofs, +\* consensusHeight : Proofs + ] + +noMsg == [ type |-> "none" ] + + +(*************************************************************************** + Helper operators. + Partly cf. ibc-rs/#55. + ***************************************************************************) + + +GetLocalConnectionID(chainID) == + IF chainID = "chainA" + THEN "connAtoB" + ELSE IF chainID = "chainB" + THEN "connBtoA" + ELSE nullConnectionID + + +\* get the connection ID of the connection end at chainID's counterparty chain +GetRemoteConnectionID(chainID) == + IF chainID = "chainA" + THEN "connBtoA" + ELSE IF chainID = "chainB" + THEN "connAtoB" + ELSE nullConnectionID + + +GetLocalClientID(chainID) == + IF chainID = "chainA" + THEN "clientA" + ELSE IF chainID = "chainB" + THEN "clientB" + ELSE nullClientID + + +\* get the connection ID of the connection end at chainID's counterparty chain +GetRemoteClientID(chainID) == + IF chainID = "chainA" + THEN "clientB" + ELSE IF chainID = "chainB" + THEN "clientA" + ELSE nullClientID + + +validConnectionParameters(para) == + /\ para.localEnd.connectionID = GetLocalConnectionID(store.id) + /\ para.remoteEnd.connectionID = GetRemoteConnectionID(store.id) + /\ para.localEnd.clientID = GetLocalClientID(store.id) + /\ para.remoteEnd.clientID = GetRemoteClientID(store.id) + /\ TRUE + + +(*************************************************************************** + Connection Handshake Module actions. + ***************************************************************************) + + +handleInitMsg == + /\ inMsg.type = "ConnOpenInit" + /\ [newState |-> TRUE, + outMsg |-> noMsg ] + + +handleTryMsg == + /\ inMsg.type = "ConnOpenTry" + /\ store.connection.state \in {"UNINIT", "INIT", "TRYOPEN"} + /\ [newState |-> TRUE, + outMsg |-> noMsg ] + + +\* If MaxHeight is not yet reached, then advance the height of the chain. +advanceChainHeight == + /\ PrintT([msg |-> "in advanceChainHeight", id |-> store.id]) + /\ store.height < MaxHeight + /\ store' = [store EXCEPT !.height = @ + 1] + /\ UNCHANGED <> + + +\* Generic handle for any time of inbound message. +handleInMsg == + /\ PrintT([msg |-> "handleInMsg", id |-> store.id]) + /\ inMsg /= noMsg + /\ IF validConnectionParameters(inMsg.parameters) = TRUE + \* The connection parameters are valid. Handle the message. + THEN LET res == \/ handleInitMsg + \/ handleTryMsg + IN /\ store' = res.newState + /\ outMsg' = res.outMsg + /\ inMsg' = noMsg + \* The connection parameters are not valid. No state transition. + ELSE /\ inMsg' = noMsg + /\ outMsg' = noMsg + /\ UNCHANGED store + + +(*************************************************************************** + Connection Handshake Module main spec. + ***************************************************************************) + +Init(chainID) == + store = [id |-> chainID, + height |-> 1, + connection |-> nullConnection ] + + +Next == + \/ advanceChainHeight + \/ handleInMsg + +============================================================================= +\* Modification History +\* Last modified Tue Apr 28 11:18:22 CEST 2020 by adi +\* Created Fri Apr 24 19:08:19 CEST 2020 by adi + diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla new file mode 100644 index 0000000000..8354ce9b43 --- /dev/null +++ b/verification/spec/connection-handshake/Environment.tla @@ -0,0 +1,108 @@ +---------------------------- MODULE Environment ---------------------------- + +EXTENDS Naturals, FiniteSets + + +CONSTANT MaxHeight \* Maximum height of any chain in the system. + + +VARIABLES + turn, + msgToA, + msgToB, + chainAStore, + chainBStore + + +vars == <> + + +chmA == INSTANCE ConnectionHandshakeModule + WITH MaxHeight <- MaxHeight, + inMsg <- msgToA, + outMsg <- msgToB, + store <- chainAStore + + +chmB == INSTANCE ConnectionHandshakeModule + WITH MaxHeight <- MaxHeight, + inMsg <- msgToB, \* Flip the message buffers w.r.t. chain A buffers. + outMsg <- msgToA, \* Inbound for "A" is outbound for "B". + store <- chainBStore + + +(*************************************************************************** + Environment actions. + ***************************************************************************) + +InitEnv == + /\ msgToA = chmA!noMsg + /\ msgToB = chmB!noMsg + + +NextEnv == + \/ /\ msgToA' \in chmA!ConnectionHandshakeMessages + /\ UNCHANGED msgToB + \/ /\ msgToB' \in chmB!ConnectionHandshakeMessages + /\ UNCHANGED msgToA + + +(****************************************************************************** + Main spec. + The system comprises the connection handshake module & environment. + *****************************************************************************) + +\* Turn-flipping mechanism. +\* This mechanism ensures that the turn goes round-robin the following order: +\* env -> chmA -> chmB -> env -> ... +FlipTurn == + turn' = ( + IF turn = "chmA" + THEN "chmB" \* After A goes B. + ELSE IF turn = "chmB" + THEN "env" \* After B goes the environment. + ELSE "chmA" \* After env goes A. + ) + + +Init == + /\ turn = "env" \* Initially, the environment takes a turn. + /\ InitEnv + /\ chmA!Init("chainA") + /\ chmB!Init("chainB") + + +\* The two CH modules and the environment alternate their steps. +Next == + /\ FlipTurn + /\ IF turn = "env" + THEN /\ NextEnv + /\ UNCHANGED <> + ELSE IF turn = "chmA" + THEN /\ chmA!Next + /\ UNCHANGED chainBStore + ELSE /\ chmB!Next + /\ UNCHANGED chainAStore + \* Handle the exceptional case when turn is neither of chm or env? + + +Spec == + /\ Init + /\ [][Next]_<> + /\ WF_turn(FlipTurn) + + +TypeInvariant == + /\ turn \in {"env", "chmA", "chmB"} + /\ msgToA \in chmA!ConnectionHandshakeMessages + /\ msgToB \in chmB!ConnectionHandshakeMessages + + +\* Model check it! +THEOREM Spec => []TypeInvariant + +============================================================================= +\* Modification History +\* Last modified Tue Apr 28 11:19:14 CEST 2020 by adi +\* Created Fri Apr 24 18:51:07 CEST 2020 by adi + diff --git a/verification/spec/connection-handshake/chm.tla b/verification/spec/connection-handshake/chm.tla deleted file mode 100644 index 1d5112be63..0000000000 --- a/verification/spec/connection-handshake/chm.tla +++ /dev/null @@ -1,237 +0,0 @@ --------------------------------- MODULE CHM -------------------------------- - -EXTENDS Naturals, FiniteSets - - -CONSTANT MaxHeight \* Maximum height of all the chains in the system. - - -VARIABLES - chModule, \* The state of this connection handshake module (chm). - turn, \* Keep track of who takes a step: either chModule or environment. - inMsg, \* A buffer holding any message incoming to the chModule. - outMsg \* A buffer holding any message outbound from the chModule. - - -vars == <> - -Heights == 1..MaxHeight - -ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} - -ConnectionIDs == {"connAtoB", "connBtoA"} -ClientIDs == { "clientA", "clientB" } - -Null == "none" -noMsg == [ type |-> "none" ] -noErr == "errNone" - - -(***************************** ConnectionEnds ***************************** - A set of connection end records. - A connection end record contains the following fields: - - - connectionID -- a connection identifier - Stores the connection identifier for this connection end. - - - clientID -- a client identifier - Stores the client identifier associated with this connection end. - - Optional, add later: - - prefix - - ***************************************************************************) -ConnectionEnds == - [ - connectionID : ConnectionIDs \union {Null}, - clientID : ClientIDs \union {Null} - ] - - -(***************************** Connections ***************************** - A set of connection records. - A connection record contains the following fields: - - - localEnd -- a connection end - Stores the connection end for the local chain. - - - remoteEnd -- a connection end - Stores the connection end for the remote chain. - ***************************************************************************) -Connections == - [ - localEnd : ConnectionEnds, - remoteEnd : ConnectionEnds - ] - - -(***************************** ConnectionHandshakeModules ****************** - A set of records defining the CHM. - A CHM record contains the following fields: - - - connectionState -- a string - Stores the current state of this connection. It has one of the - following values: "UNINIT", "INIT", "TRYOPEN", "OPEN". - - - connection -- a connection with two ends - Stores the connection. - - - chainHeight -- a height - Stores the height of the chain running alongside the CHM. - ***************************************************************************) -ConnectionHandshakeModules == - [ - connectionState : ConnectionStates, - connection : Connections, - chainHeight : Heights - \* light client, consensus state, etc.. - ] - - - -(******************************** Messages ******************************** -These messages are connection handshake specific. - ***************************************************************************) -ConnectionHandshakeMessages == - [type : {"ConnOpenInit"}, - connection : Connections] -\* \union -\* [type : {"ConnOpenTry"}, -\* connection : Connections, -\* stateProof : Proofs, -\* consensusHeight : Proofs] - - - -(*************************************************************************** - Connection handshake message handlers. -***************************************************************************) - -\* The chModule handles a "ConnOpenInit" message. -connectionOpenInit(chm, initMsg) == - \* If we're in the right state. - IF chm.connectionState = "UNINIT" THEN - [initCHM |-> [chm EXCEPT !.connection = initMsg.connection, - !.connectionState = "INIT"], - msg |-> [type |-> "ConnOpenTry", - connection |-> initMsg.connection]] - \* TODO: Proofs go here. - \* Otherwise, do not update the chain - ELSE [initCHM |-> chm, - msg |-> noMsg ] - -handleMsgConnectionOpenInit == - /\ inMsg.type = "ConnOpenInit" - /\ LET res == connectionOpenInit(chModule, inMsg) IN - /\ chModule' = res.initCHM - /\ outMsg' = res.msg - /\ inMsg' = noMsg \* The chm consumed its inbound message. - -(*************************************************************************** - Connection Handshake Module actions. - ***************************************************************************) - -\* Advance the height of the chain if MaxHeight is not yet reached. -advanceChainHeight == - /\ chModule.chainHeight < MaxHeight - /\ chModule' = [chModule EXCEPT - !.chainHeight = chModule.chainHeight + 1] - /\ UNCHANGED <> - - -(*** - * Initial value for a connection end. - **) -ConnectionEndInitValue == - [connectionID |-> Null, - clientID |-> Null] - - -(*** - * Initial values for a connection. - **) -ConnectionInitValue == - [localEnd |-> ConnectionEndInitValue, - remoteEnd |-> ConnectionEndInitValue] - -CHModuleInitValue == - [chainHeight |-> 1, - connectionState |-> "UNINT", - connection |-> ConnectionInitValue ] - -InitCHModule == - /\ chModule = CHModuleInitValue - /\ outMsg = noMsg - -NextCHModule == - \/ advanceChainHeight - \/ handleMsgConnectionOpenInit - -(*************************************************************************** - Environment actions - ***************************************************************************) - - InitEnv == - /\ inMsg = noMsg - - OnConnInitMsg == - /\ inMsg' \in [type: {"ConnOpenInit"}, - connection : Connections] - - NextEnv == - \/ OnConnInitMsg - - -(****************************************************************************** - Main spec. - The system comprises the connection handshake module & environment. - *****************************************************************************) - -Init == - turn = "env" \* Initially, the environment takes a turn. - /\ InitEnv - /\ InitCHModule - - -\* Turn-flipping mechanism. -FlipTurn == - turn' = ( - IF turn = "chm" THEN - "env" - ELSE - "chm" - ) - - -\* chModule and environment alternate their steps -Next == -/\ FlipTurn -/\ IF turn = "chm" THEN - /\ NextCHModule - ELSE - /\ NextEnv - /\ outMsg' = noMsg \* The env. consumed the outbound message. - /\ UNCHANGED chModule - \* Handle the exceptional case when turn is neither of chm or env? - -Spec == - Init - /\ [][Next]_<> - /\ WF_turn(FlipTurn) - - -TypeInvariant == - /\ inMsg \in ConnectionHandshakeMessages \union { noMsg } - - -\* Model check it! -THEOREM Spec => []TypeInvariant - - -============================================================================= -\* Modification History -\* Last modified Thu Apr 23 11:22:41 CEST 2020 by adi -\* Wed Apr 22 10:42:14 CEST 2020 improvements & suggestions by anca & zarko -\* Created Fri Apr 17 10:28:22 CEST 2020 by adi - - From 7fd34a1b5815ee383e36a05a2cbbbae8d5bbc70a Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 28 Apr 2020 16:44:15 +0200 Subject: [PATCH 24/63] Fixed bug; still have the bug. --- .../ConnectionHandshakeModule.tla | 88 +++++++++++++------ .../spec/connection-handshake/Environment.tla | 4 +- 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index ab2449d349..cff362a614 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -38,20 +38,27 @@ Connections == [ parameters : ConnectionParameters, state : ConnectionStates - \* any other fields that are specific for a connection ] nullConnection == "null" (******************************** Messages ******************************** -These messages are connection handshake specific. + These messages are connection handshake specific. + + In the low-level connection handshake protocol, the four messages have the + following types: ConnOpenInit, ConnOpenTry, ConnOpenAck, ConnOpenConfirm. + These are described in ICS 003. + In this high-level specification, we choose slightly different names, to + make an explicit distinction to the low-level protocol. Message types + are as follows: CHMsgInit, CHMsgTry, CHMsgAck, and CHMsgConfirm. Notice that + the fields of each message are also different to the ICS 003 specification. ***************************************************************************) ConnectionHandshakeMessages == - [type : {"ConnOpenInit"}, + [type : {"CHMsgInit"}, parameters : ConnectionParameters] \union - [type : {"ConnOpenTry"}, + [type : {"CHMsgTry"}, parameters : ConnectionParameters \* stateProof : Proofs, \* consensusHeight : Proofs @@ -100,12 +107,20 @@ GetRemoteClientID(chainID) == ELSE nullClientID -validConnectionParameters(para) == +\* Validates a ConnectionParameter `para` against the local store. +\* Returns true if `para` is valid, and false otherwise. +ValidConnectionParameters(para) == /\ para.localEnd.connectionID = GetLocalConnectionID(store.id) /\ para.remoteEnd.connectionID = GetRemoteConnectionID(store.id) /\ para.localEnd.clientID = GetLocalClientID(store.id) /\ para.remoteEnd.clientID = GetRemoteClientID(store.id) - /\ TRUE + + +\* Given a ConnectionParameters record `para`, this operator returns a new set +\* of parameters where the local and remote ends are flipped (i.e., reversed). +FlipConnectionParameters(para) == + [localEnd |-> para.remoteEnd, + remoteEnd |-> para.localEnd] (*************************************************************************** @@ -113,42 +128,57 @@ validConnectionParameters(para) == ***************************************************************************) -handleInitMsg == - /\ inMsg.type = "ConnOpenInit" - /\ [newState |-> TRUE, - outMsg |-> noMsg ] +\* Handles a `CHMsgInit` message. +handleInitMsg(m) == + IF store.connection = nullConnection \/ store.connection.state = "INIT" + THEN [nConnection |-> [parameters |-> m.parameters, + state |-> "INIT"], + (* Asemble the outbound message, type: HandshakeTry *) + oMsg |-> [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgTry"]] + (* TODO: add proofs here. *) + ELSE [nConnection |-> store.connection, + oMsg |-> noMsg] -handleTryMsg == - /\ inMsg.type = "ConnOpenTry" - /\ store.connection.state \in {"UNINIT", "INIT", "TRYOPEN"} - /\ [newState |-> TRUE, - outMsg |-> noMsg ] +handleTryMsg(m) == + (**** TODO ****) + [nConnection |-> store.connection, + oMsg |-> noMsg ] \* If MaxHeight is not yet reached, then advance the height of the chain. advanceChainHeight == - /\ PrintT([msg |-> "in advanceChainHeight", id |-> store.id]) /\ store.height < MaxHeight /\ store' = [store EXCEPT !.height = @ + 1] /\ UNCHANGED <> +(****** + Expects a valid ConnectionHandshakeMessage record. + Does two basic actions: + 1. Updates the chain store, and + 2. Updates outMsg with a reply message. + *****) +ProcessConnectionHandshakeMessage(msg) == + LET res == CASE msg.type = "CHMsgInit" -> handleInitMsg(msg) + [] msg.type = "CHMsgTry" -> handleTryMsg(msg) + IN /\ PrintT([msg |-> res]) + /\ store' = [store EXCEPT !.connection = res.nConnection] + /\ outMsg' = res.oMsg + + \* Generic handle for any time of inbound message. -handleInMsg == - /\ PrintT([msg |-> "handleInMsg", id |-> store.id]) +processInMsg == /\ inMsg /= noMsg - /\ IF validConnectionParameters(inMsg.parameters) = TRUE - \* The connection parameters are valid. Handle the message. - THEN LET res == \/ handleInitMsg - \/ handleTryMsg - IN /\ store' = res.newState - /\ outMsg' = res.outMsg - /\ inMsg' = noMsg + /\ inMsg \in ConnectionHandshakeMessages + /\ IF ValidConnectionParameters(inMsg.parameters) = TRUE + THEN /\ ProcessConnectionHandshakeMessage(inMsg) \* The connection parameters are not valid. No state transition. - ELSE /\ inMsg' = noMsg - /\ outMsg' = noMsg + ELSE /\ outMsg' = noMsg /\ UNCHANGED store + /\ inMsg' = noMsg \* Flush the inbound message buffer. + (*************************************************************************** @@ -163,10 +193,10 @@ Init(chainID) == Next == \/ advanceChainHeight - \/ handleInMsg + \/ processInMsg ============================================================================= \* Modification History -\* Last modified Tue Apr 28 11:18:22 CEST 2020 by adi +\* Last modified Tue Apr 28 16:02:34 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 8354ce9b43..c97ea9affe 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -45,7 +45,7 @@ NextEnv == /\ UNCHANGED msgToB \/ /\ msgToB' \in chmB!ConnectionHandshakeMessages /\ UNCHANGED msgToA - + (****************************************************************************** Main spec. @@ -103,6 +103,6 @@ THEOREM Spec => []TypeInvariant ============================================================================= \* Modification History -\* Last modified Tue Apr 28 11:19:14 CEST 2020 by adi +\* Last modified Tue Apr 28 16:41:12 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From f6104681eb1b9bca341ef8061dc375ca06251861 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 29 Apr 2020 16:04:19 +0200 Subject: [PATCH 25/63] After reviewing with Anca & Zarko. --- .../ConnectionHandshakeModule.tla | 56 +++++++++++++------ .../spec/connection-handshake/Environment.tla | 39 ++++++++----- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index cff362a614..aa6f36047d 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -12,15 +12,19 @@ VARIABLES store \* The local store of the chain running this chModule. - ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} + +(* TODO: constants *) ConnectionIDs == {"connAtoB", "connBtoA"} nullConnectionID == "null" + +(* TODO: constants *) ClientIDs == { "clientA", "clientB" } nullClientID == "null" + ConnectionEnds == [ connectionID : ConnectionIDs, @@ -40,8 +44,19 @@ Connections == state : ConnectionStates ] -nullConnection == "null" +nullConnection == [state |-> "UNINIT"] + +Heights == 1..MaxHeight + + +Clients == + [ + clientID : ClientIDs, + consensusState : Heights + ] + +nullClient == [clientID |-> nullClientID] (******************************** Messages ******************************** These messages are connection handshake specific. @@ -110,10 +125,10 @@ GetRemoteClientID(chainID) == \* Validates a ConnectionParameter `para` against the local store. \* Returns true if `para` is valid, and false otherwise. ValidConnectionParameters(para) == - /\ para.localEnd.connectionID = GetLocalConnectionID(store.id) - /\ para.remoteEnd.connectionID = GetRemoteConnectionID(store.id) - /\ para.localEnd.clientID = GetLocalClientID(store.id) - /\ para.remoteEnd.clientID = GetRemoteClientID(store.id) + /\ para.localEnd.connectionID = GetLocalConnectionID(store.id) + /\ para.remoteEnd.connectionID = GetRemoteConnectionID(store.id) + /\ para.localEnd.clientID = GetLocalClientID(store.id) + /\ para.remoteEnd.clientID = GetRemoteClientID(store.id) \* Given a ConnectionParameters record `para`, this operator returns a new set @@ -130,7 +145,7 @@ FlipConnectionParameters(para) == \* Handles a `CHMsgInit` message. handleInitMsg(m) == - IF store.connection = nullConnection \/ store.connection.state = "INIT" + IF store.connection.state = "UNINIT" THEN [nConnection |-> [parameters |-> m.parameters, state |-> "INIT"], (* Asemble the outbound message, type: HandshakeTry *) @@ -148,7 +163,7 @@ handleTryMsg(m) == \* If MaxHeight is not yet reached, then advance the height of the chain. -advanceChainHeight == +AdvanceChainHeight == /\ store.height < MaxHeight /\ store' = [store EXCEPT !.height = @ + 1] /\ UNCHANGED <> @@ -168,19 +183,20 @@ ProcessConnectionHandshakeMessage(msg) == /\ outMsg' = res.oMsg -\* Generic handle for any time of inbound message. -processInMsg == - /\ inMsg /= noMsg - /\ inMsg \in ConnectionHandshakeMessages +\* Generic handle for any type of inbound message. +\* Assumes that 'inMsg' is not empty. +\* Takes care of changing the 'store' and 'outMsg'. +ProcessInMsg == /\ IF ValidConnectionParameters(inMsg.parameters) = TRUE - THEN /\ ProcessConnectionHandshakeMessage(inMsg) + THEN ProcessConnectionHandshakeMessage(inMsg) \* The connection parameters are not valid. No state transition. - ELSE /\ outMsg' = noMsg + ELSE /\ outMsg' = noMsg \* No reply. /\ UNCHANGED store /\ inMsg' = noMsg \* Flush the inbound message buffer. + (*************************************************************************** Connection Handshake Module main spec. ***************************************************************************) @@ -188,15 +204,19 @@ processInMsg == Init(chainID) == store = [id |-> chainID, height |-> 1, - connection |-> nullConnection ] + connection |-> nullConnection, + client |-> nullClient] Next == - \/ advanceChainHeight - \/ processInMsg + IF inMsg /= noMsg + THEN ProcessInMsg + \* We have no input message, nothing for us to do. + ELSE UNCHANGED <> + ============================================================================= \* Modification History -\* Last modified Tue Apr 28 16:02:34 CEST 2020 by adi +\* Last modified Wed Apr 29 15:47:07 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index c97ea9affe..7a42241648 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -27,7 +27,7 @@ chmA == INSTANCE ConnectionHandshakeModule chmB == INSTANCE ConnectionHandshakeModule WITH MaxHeight <- MaxHeight, inMsg <- msgToB, \* Flip the message buffers w.r.t. chain A buffers. - outMsg <- msgToA, \* Inbound for "A" is outbound for "B". + outMsg <- msgToA, \* Inbound for "A" is outbound for "B". store <- chainBStore @@ -35,18 +35,28 @@ chmB == INSTANCE ConnectionHandshakeModule Environment actions. ***************************************************************************) + InitEnv == /\ msgToA = chmA!noMsg /\ msgToB = chmB!noMsg -NextEnv == +GoodNextEnv == + UNCHANGED <> + + +MaliciousNextEnv == \/ /\ msgToA' \in chmA!ConnectionHandshakeMessages /\ UNCHANGED msgToB \/ /\ msgToB' \in chmB!ConnectionHandshakeMessages /\ UNCHANGED msgToA +(* Eventually, just good. *) +NextEnv == + \/ GoodNextEnv + \/ MaliciousNextEnv + (****************************************************************************** Main spec. The system comprises the connection handshake module & environment. @@ -74,17 +84,18 @@ Init == \* The two CH modules and the environment alternate their steps. Next == - /\ FlipTurn - /\ IF turn = "env" - THEN /\ NextEnv - /\ UNCHANGED <> - ELSE IF turn = "chmA" - THEN /\ chmA!Next - /\ UNCHANGED chainBStore - ELSE /\ chmB!Next - /\ UNCHANGED chainAStore - \* Handle the exceptional case when turn is neither of chm or env? - + IF chainAStore.connection.state = "OPEN" /\ chainBStore.connection.state = "OPEN" + THEN UNCHANGED <> + ELSE /\ FlipTurn + /\ IF turn = "env" + THEN /\ NextEnv + /\ UNCHANGED <> + ELSE IF turn = "chmA" + THEN /\ chmA!Next + /\ UNCHANGED chainBStore + ELSE /\ chmB!Next + /\ UNCHANGED chainAStore + Spec == /\ Init @@ -103,6 +114,6 @@ THEOREM Spec => []TypeInvariant ============================================================================= \* Modification History -\* Last modified Tue Apr 28 16:41:12 CEST 2020 by adi +\* Last modified Wed Apr 29 15:52:53 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From c8fff52df4b7348e7450a0f4d2c28372955040f6 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Fri, 1 May 2020 17:15:56 +0200 Subject: [PATCH 26/63] Initial versions of liveness & safety properties; not much progress on WF --- .../ConnectionHandshakeModule.tla | 98 ++++++------------- .../spec/connection-handshake/Environment.tla | 76 +++++++++----- 2 files changed, 81 insertions(+), 93 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index aa6f36047d..023bd71d71 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -1,8 +1,8 @@ --------------------- MODULE ConnectionHandshakeModule --------------------- -EXTENDS Naturals, TLC +EXTENDS Naturals -\* Maximum height of the chain where this moduele is running. +\* Maximum height of the chain where this module is running. CONSTANT MaxHeight @@ -55,7 +55,7 @@ Clients == clientID : ClientIDs, consensusState : Heights ] - + nullClient == [clientID |-> nullClientID] (******************************** Messages ******************************** @@ -72,7 +72,9 @@ nullClient == [clientID |-> nullClientID] ConnectionHandshakeMessages == [type : {"CHMsgInit"}, parameters : ConnectionParameters] + \union + [type : {"CHMsgTry"}, parameters : ConnectionParameters \* stateProof : Proofs, @@ -84,51 +86,19 @@ noMsg == [ type |-> "none" ] (*************************************************************************** Helper operators. - Partly cf. ibc-rs/#55. ***************************************************************************) -GetLocalConnectionID(chainID) == - IF chainID = "chainA" - THEN "connAtoB" - ELSE IF chainID = "chainB" - THEN "connBtoA" - ELSE nullConnectionID - - -\* get the connection ID of the connection end at chainID's counterparty chain -GetRemoteConnectionID(chainID) == - IF chainID = "chainA" - THEN "connBtoA" - ELSE IF chainID = "chainB" - THEN "connAtoB" - ELSE nullConnectionID - - -GetLocalClientID(chainID) == - IF chainID = "chainA" - THEN "clientA" - ELSE IF chainID = "chainB" - THEN "clientB" - ELSE nullClientID - - -\* get the connection ID of the connection end at chainID's counterparty chain -GetRemoteClientID(chainID) == - IF chainID = "chainA" - THEN "clientB" - ELSE IF chainID = "chainB" - THEN "clientA" - ELSE nullClientID - - -\* Validates a ConnectionParameter `para` against the local store. -\* Returns true if `para` is valid, and false otherwise. +\* Validates a ConnectionParameter `para` against an already existing connection. +\* If the connection in the store is `nullConnection`, returns true. +\* Otherwise, returns true if `para` matches the connection in the store, and false otherwise. ValidConnectionParameters(para) == - /\ para.localEnd.connectionID = GetLocalConnectionID(store.id) - /\ para.remoteEnd.connectionID = GetRemoteConnectionID(store.id) - /\ para.localEnd.clientID = GetLocalClientID(store.id) - /\ para.remoteEnd.clientID = GetRemoteClientID(store.id) + \/ store.connection = nullConnection + \/ /\ store.connection /= nullConnection + /\ store.connection.parameters.localEnd.connectionID = para.localEnd.connectionID + /\ store.connection.parameters.remoteEnd.connectionID = para.remoteEnd.connectionID + /\ store.connection.parameters.localEnd.clientID = para.localEnd.clientID + /\ store.connection.parameters.remoteEnd.clientID = para.remoteEnd.clientID \* Given a ConnectionParameters record `para`, this operator returns a new set @@ -139,24 +109,25 @@ FlipConnectionParameters(para) == (*************************************************************************** - Connection Handshake Module actions. + Connection Handshake Module actions & operators. ***************************************************************************) -\* Handles a `CHMsgInit` message. -handleInitMsg(m) == - IF store.connection.state = "UNINIT" - THEN [nConnection |-> [parameters |-> m.parameters, - state |-> "INIT"], - (* Asemble the outbound message, type: HandshakeTry *) - oMsg |-> [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgTry"]] - (* TODO: add proofs here. *) - ELSE [nConnection |-> store.connection, - oMsg |-> noMsg] +\* Handles a `CHMsgInit` message. +HandleInitMsg(m) == + (* TODO: add proofs in the THEN branch. *) + LET res == IF store.connection.state = "UNINIT" + THEN [nConnection |-> [parameters |-> m.parameters, + state |-> "INIT"], + oMsg |-> [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgTry"]] + ELSE [nConnection |-> store.connection, + oMsg |-> noMsg] + IN /\ store' = [store EXCEPT !.connection = res.nConnection] + /\ outMsg' = res.oMsg -handleTryMsg(m) == +HandleTryMsg(m) == (**** TODO ****) [nConnection |-> store.connection, oMsg |-> noMsg ] @@ -173,14 +144,11 @@ AdvanceChainHeight == Expects a valid ConnectionHandshakeMessage record. Does two basic actions: 1. Updates the chain store, and - 2. Updates outMsg with a reply message. + 2. Updates outMsg with a reply message, possibly NoMsg. *****) ProcessConnectionHandshakeMessage(msg) == - LET res == CASE msg.type = "CHMsgInit" -> handleInitMsg(msg) - [] msg.type = "CHMsgTry" -> handleTryMsg(msg) - IN /\ PrintT([msg |-> res]) - /\ store' = [store EXCEPT !.connection = res.nConnection] - /\ outMsg' = res.oMsg + \/ "CHMsgInit" /\ HandleInitMsg(msg) + \/ "CHMsgTry" /\ HandleTryMsg(msg) \* Generic handle for any type of inbound message. @@ -195,8 +163,6 @@ ProcessInMsg == /\ inMsg' = noMsg \* Flush the inbound message buffer. - - (*************************************************************************** Connection Handshake Module main spec. ***************************************************************************) @@ -217,6 +183,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Wed Apr 29 15:47:07 CEST 2020 by adi +\* Last modified Fri May 01 17:13:55 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 7a42241648..4f7e94ba75 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -8,27 +8,27 @@ CONSTANT MaxHeight \* Maximum height of any chain in the system. VARIABLES turn, - msgToA, - msgToB, - chainAStore, - chainBStore + bufChainA, \* A buffer for messages inbound to chain A. + bufChainB, + storeChainA, \* The local store of chain A. + storeChainB -vars == <> +vars == <> chmA == INSTANCE ConnectionHandshakeModule WITH MaxHeight <- MaxHeight, - inMsg <- msgToA, - outMsg <- msgToB, - store <- chainAStore + inMsg <- bufChainA, + outMsg <- bufChainB, + store <- storeChainA chmB == INSTANCE ConnectionHandshakeModule WITH MaxHeight <- MaxHeight, - inMsg <- msgToB, \* Flip the message buffers w.r.t. chain A buffers. - outMsg <- msgToA, \* Inbound for "A" is outbound for "B". - store <- chainBStore + inMsg <- bufChainB, \* Flip the message buffers w.r.t. chain A buffers. + outMsg <- bufChainA, \* Inbound for "A" is outbound for "B". + store <- storeChainB (*************************************************************************** @@ -37,26 +37,27 @@ chmB == INSTANCE ConnectionHandshakeModule InitEnv == - /\ msgToA = chmA!noMsg - /\ msgToB = chmB!noMsg + /\ bufChainA = chmA!noMsg + /\ bufChainB = chmB!noMsg GoodNextEnv == - UNCHANGED <> + UNCHANGED <> MaliciousNextEnv == - \/ /\ msgToA' \in chmA!ConnectionHandshakeMessages - /\ UNCHANGED msgToB - \/ /\ msgToB' \in chmB!ConnectionHandshakeMessages - /\ UNCHANGED msgToA + \/ /\ bufChainA' \in chmA!ConnectionHandshakeMessages + /\ UNCHANGED bufChainB + \/ /\ bufChainB' \in chmB!ConnectionHandshakeMessages + /\ UNCHANGED bufChainA -(* Eventually, just good. *) +(* TODO: Eventually, it should be just 'good'. *) NextEnv == \/ GoodNextEnv \/ MaliciousNextEnv + (****************************************************************************** Main spec. The system comprises the connection handshake module & environment. @@ -84,18 +85,18 @@ Init == \* The two CH modules and the environment alternate their steps. Next == - IF chainAStore.connection.state = "OPEN" /\ chainBStore.connection.state = "OPEN" + IF storeChainA.connection.state = "OPEN" /\ storeChainB.connection.state = "OPEN" THEN UNCHANGED <> ELSE /\ FlipTurn /\ IF turn = "env" THEN /\ NextEnv - /\ UNCHANGED <> + /\ UNCHANGED <> ELSE IF turn = "chmA" THEN /\ chmA!Next - /\ UNCHANGED chainBStore + /\ UNCHANGED storeChainB ELSE /\ chmB!Next - /\ UNCHANGED chainAStore - + /\ UNCHANGED storeChainA + Spec == /\ Init @@ -105,15 +106,36 @@ Spec == TypeInvariant == /\ turn \in {"env", "chmA", "chmB"} - /\ msgToA \in chmA!ConnectionHandshakeMessages - /\ msgToB \in chmB!ConnectionHandshakeMessages + /\ bufChainA \in chmA!ConnectionHandshakeMessages + /\ bufChainB \in chmB!ConnectionHandshakeMessages \* Model check it! THEOREM Spec => []TypeInvariant + +\* Liveness property. +Termination == + <> /\ storeChainA.connection.state = "INIT" + /\ storeChainA.connection.state = "INIT" + + +\* Safety property. +\* If the connections in the two chains are not null, then the +\* connection parameters must always match. +(* TODO: This should be incorporated in the THEOREM block. + Remove the box [] from this expression and do a conjuct w/ TypeInvariant: + THEOREM Spec => [](TypeInvarianta /\ Consistency) + See §5.7 of Lamport's SS book. *) +Consistency == + /\ storeChainA.connection /= chmA!nullConnection + /\ storeChainB.connection /= chmB!nullConnection + => [] (storeChainA.connection.parameters + = chmB!FlipConnectionParameters(storeChainB.connection.parameters)) + + ============================================================================= \* Modification History -\* Last modified Wed Apr 29 15:52:53 CEST 2020 by adi +\* Last modified Fri May 01 15:45:17 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From d598cc45b3b8fd1011dc5ef0a0ae1c260711a6fc Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Mon, 4 May 2020 14:57:24 +0200 Subject: [PATCH 27/63] Simplifications based on suggestion from Igor&Anca. --- .../ConnectionHandshakeModule.tla | 8 +- .../spec/connection-handshake/Environment.tla | 88 +++++++------------ 2 files changed, 39 insertions(+), 57 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 023bd71d71..3049bbd6cc 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -83,6 +83,8 @@ ConnectionHandshakeMessages == noMsg == [ type |-> "none" ] +InitMsg == + TRUE (*************************************************************************** Helper operators. @@ -147,8 +149,8 @@ AdvanceChainHeight == 2. Updates outMsg with a reply message, possibly NoMsg. *****) ProcessConnectionHandshakeMessage(msg) == - \/ "CHMsgInit" /\ HandleInitMsg(msg) - \/ "CHMsgTry" /\ HandleTryMsg(msg) + \/ msg.type = "CHMsgInit" /\ HandleInitMsg(msg) + \/ msg.type = "CHMsgTry" /\ HandleTryMsg(msg) \* Generic handle for any type of inbound message. @@ -183,6 +185,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Fri May 01 17:13:55 CEST 2020 by adi +\* Last modified Mon May 04 14:56:24 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 4f7e94ba75..b290125c64 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -7,14 +7,17 @@ CONSTANT MaxHeight \* Maximum height of any chain in the system. VARIABLES - turn, bufChainA, \* A buffer for messages inbound to chain A. bufChainB, storeChainA, \* The local store of chain A. - storeChainB + storeChainB, + stillMalicious -vars == <> +chainAVars == <> +chainBVars == <> +chainVars == <> +allVars == <> chmA == INSTANCE ConnectionHandshakeModule @@ -37,12 +40,9 @@ chmB == INSTANCE ConnectionHandshakeModule InitEnv == - /\ bufChainA = chmA!noMsg - /\ bufChainB = chmB!noMsg - - -GoodNextEnv == - UNCHANGED <> + /\ stillMalicious = TRUE + /\ \/ bufChainA = chmA!InitMsg \* chmA!noMsg + \/ bufChainB = chmB!InitMsg \* chmB!noMsg MaliciousNextEnv == @@ -54,30 +54,22 @@ MaliciousNextEnv == (* TODO: Eventually, it should be just 'good'. *) NextEnv == - \/ GoodNextEnv - \/ MaliciousNextEnv + \/ stillMalicious /\ MaliciousNextEnv /\ UNCHANGED stillMalicious + \/ stillMalicious /\ stillMalicious' = FALSE + +CHDone == + /\ storeChainA.connection.state = "OPEN" + /\ storeChainB.connection.state = "OPEN" + /\ UNCHANGED <> (****************************************************************************** Main spec. The system comprises the connection handshake module & environment. *****************************************************************************) -\* Turn-flipping mechanism. -\* This mechanism ensures that the turn goes round-robin the following order: -\* env -> chmA -> chmB -> env -> ... -FlipTurn == - turn' = ( - IF turn = "chmA" - THEN "chmB" \* After A goes B. - ELSE IF turn = "chmB" - THEN "env" \* After B goes the environment. - ELSE "chmA" \* After env goes A. - ) - Init == - /\ turn = "env" \* Initially, the environment takes a turn. /\ InitEnv /\ chmA!Init("chainA") /\ chmB!Init("chainB") @@ -85,57 +77,45 @@ Init == \* The two CH modules and the environment alternate their steps. Next == - IF storeChainA.connection.state = "OPEN" /\ storeChainB.connection.state = "OPEN" - THEN UNCHANGED <> - ELSE /\ FlipTurn - /\ IF turn = "env" - THEN /\ NextEnv - /\ UNCHANGED <> - ELSE IF turn = "chmA" - THEN /\ chmA!Next - /\ UNCHANGED storeChainB - ELSE /\ chmB!Next - /\ UNCHANGED storeChainA + \/ CHDone + \/ NextEnv /\ UNCHANGED <> + \/ chmA!Next /\ UNCHANGED storeChainB + \/ chmB!Next /\ UNCHANGED storeChainA Spec == /\ Init - /\ [][Next]_<> - /\ WF_turn(FlipTurn) + /\ [][Next]_<> + /\ WF_chainAVars(chmA!Next) + /\ WF_chainBVars(chmB!Next) TypeInvariant == - /\ turn \in {"env", "chmA", "chmB"} /\ bufChainA \in chmA!ConnectionHandshakeMessages /\ bufChainB \in chmB!ConnectionHandshakeMessages -\* Model check it! -THEOREM Spec => []TypeInvariant - - \* Liveness property. Termination == - <> /\ storeChainA.connection.state = "INIT" - /\ storeChainA.connection.state = "INIT" + <> ~ stillMalicious + => <> /\ storeChainA.connection.state = "INIT" (* should be OPEN *) + /\ storeChainB.connection.state = "INIT" \* Safety property. \* If the connections in the two chains are not null, then the \* connection parameters must always match. -(* TODO: This should be incorporated in the THEOREM block. - Remove the box [] from this expression and do a conjuct w/ TypeInvariant: - THEOREM Spec => [](TypeInvarianta /\ Consistency) - See §5.7 of Lamport's SS book. *) +ConsistencyInv == + \/ storeChainA.connection = chmA!nullConnection + \/ storeChainB.connection = chmB!nullConnection + \/ storeChainA.connection.parameters + = chmB!FlipConnectionParameters(storeChainB.connection.parameters) + Consistency == - /\ storeChainA.connection /= chmA!nullConnection - /\ storeChainB.connection /= chmB!nullConnection - => [] (storeChainA.connection.parameters - = chmB!FlipConnectionParameters(storeChainB.connection.parameters)) - + [] ConsistencyInv ============================================================================= \* Modification History -\* Last modified Fri May 01 15:45:17 CEST 2020 by adi +\* Last modified Mon May 04 14:56:16 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 6f1f82d58bf4a92249f4906080a336ff72e4e530 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Mon, 4 May 2020 16:15:32 +0200 Subject: [PATCH 28/63] Spec model check OK. WIP: liveness & safety --- .../ConnectionHandshakeModule.tla | 35 +++++++++++++----- .../spec/connection-handshake/Environment.tla | 37 ++++++++++--------- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 3049bbd6cc..465b4b91f3 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -81,10 +81,18 @@ ConnectionHandshakeMessages == \* consensusHeight : Proofs ] -noMsg == [ type |-> "none" ] +NoMsg == [ type |-> "none" ] + +(* The initialization message that this module expects *) InitMsg == - TRUE + CHOOSE m \in ConnectionHandshakeMessages : + /\ m.type = "CHMsgInit" + /\ m.parameters.localEnd.connectionID /= m.parameters.remoteEnd.connectionID + /\ m.parameters.localEnd.clientID /= m.parameters.remoteEnd.clientID +\* [type : { "CHMsgInit" }, +\* parameters : ConnectionParameters] + (*************************************************************************** Helper operators. @@ -124,15 +132,22 @@ HandleInitMsg(m) == oMsg |-> [parameters |-> FlipConnectionParameters(m.parameters), type |-> "CHMsgTry"]] ELSE [nConnection |-> store.connection, - oMsg |-> noMsg] + oMsg |-> NoMsg] IN /\ store' = [store EXCEPT !.connection = res.nConnection] /\ outMsg' = res.oMsg HandleTryMsg(m) == - (**** TODO ****) - [nConnection |-> store.connection, - oMsg |-> noMsg ] + (* TODO: add proofs & more logic. *) + LET res == IF store.connection.state = "UNINIT" + THEN [nConnection |-> [parameters |-> m.parameters, + state |-> "INIT"], + oMsg |-> [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgAck"]] + ELSE [nConnection |-> store.connection, + oMsg |-> NoMsg] + IN /\ store' = [store EXCEPT !.connection = res.nConnection] + /\ outMsg' = res.oMsg \* If MaxHeight is not yet reached, then advance the height of the chain. @@ -160,9 +175,9 @@ ProcessInMsg == /\ IF ValidConnectionParameters(inMsg.parameters) = TRUE THEN ProcessConnectionHandshakeMessage(inMsg) \* The connection parameters are not valid. No state transition. - ELSE /\ outMsg' = noMsg \* No reply. + ELSE /\ outMsg' = NoMsg \* No reply. /\ UNCHANGED store - /\ inMsg' = noMsg \* Flush the inbound message buffer. + /\ inMsg' = NoMsg \* Flush the inbound message buffer. (*************************************************************************** @@ -177,7 +192,7 @@ Init(chainID) == Next == - IF inMsg /= noMsg + IF inMsg /= NoMsg THEN ProcessInMsg \* We have no input message, nothing for us to do. ELSE UNCHANGED <> @@ -185,6 +200,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Mon May 04 14:56:24 CEST 2020 by adi +\* Last modified Mon May 04 16:12:27 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index b290125c64..9d4ba5369a 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -8,16 +8,16 @@ CONSTANT MaxHeight \* Maximum height of any chain in the system. VARIABLES bufChainA, \* A buffer for messages inbound to chain A. - bufChainB, + bufChainB, \* A buffer for messages inbound to chain B. storeChainA, \* The local store of chain A. - storeChainB, - stillMalicious + storeChainB, \* The local store of chain B. + stillMalicious \* If TRUE, environment interferes w/ CH protocol. chainAVars == <> chainBVars == <> -chainVars == <> -allVars == <> +chainStoreVars == <> +allVars == <> chmA == INSTANCE ConnectionHandshakeModule @@ -41,8 +41,8 @@ chmB == INSTANCE ConnectionHandshakeModule InitEnv == /\ stillMalicious = TRUE - /\ \/ bufChainA = chmA!InitMsg \* chmA!noMsg - \/ bufChainB = chmB!InitMsg \* chmB!noMsg + /\ \/ bufChainA = chmA!InitMsg /\ bufChainB = chmB!NoMsg + \/ bufChainB = chmB!InitMsg /\ bufChainA = chmA!NoMsg MaliciousNextEnv == @@ -52,15 +52,18 @@ MaliciousNextEnv == /\ UNCHANGED bufChainA -(* TODO: Eventually, it should be just 'good'. *) NextEnv == - \/ stillMalicious /\ MaliciousNextEnv /\ UNCHANGED stillMalicious - \/ stillMalicious /\ stillMalicious' = FALSE + \/ /\ stillMalicious + /\ MaliciousNextEnv + /\ UNCHANGED stillMalicious + \/ /\ stillMalicious + /\ stillMalicious' = FALSE + /\ UNCHANGED<> CHDone == - /\ storeChainA.connection.state = "OPEN" - /\ storeChainB.connection.state = "OPEN" + /\ storeChainA.connection.state = "INIT" (* TODO: should be OPEN *) + /\ storeChainB.connection.state = "INIT" /\ UNCHANGED <> (****************************************************************************** @@ -78,9 +81,9 @@ Init == \* The two CH modules and the environment alternate their steps. Next == \/ CHDone - \/ NextEnv /\ UNCHANGED <> - \/ chmA!Next /\ UNCHANGED storeChainB - \/ chmB!Next /\ UNCHANGED storeChainA + \/ NextEnv /\ UNCHANGED <> + \/ chmA!Next /\ UNCHANGED <> + \/ chmB!Next /\ UNCHANGED <> Spec == @@ -98,7 +101,7 @@ TypeInvariant == \* Liveness property. Termination == <> ~ stillMalicious - => <> /\ storeChainA.connection.state = "INIT" (* should be OPEN *) + => <> /\ storeChainA.connection.state = "INIT" (* TODO: should be OPEN *) /\ storeChainB.connection.state = "INIT" @@ -116,6 +119,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Mon May 04 14:56:16 CEST 2020 by adi +\* Last modified Mon May 04 16:08:21 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From e6cb34ca44a664032800e32ffb3a300418b1ea4c Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 5 May 2020 13:31:58 +0200 Subject: [PATCH 29/63] Constant params in CHM; checking consistency --- .../ConnectionHandshakeModule.tla | 53 ++++++++----------- .../spec/connection-handshake/Environment.tla | 50 +++++++++++++---- 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 465b4b91f3..2bb8f8bb5e 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -2,8 +2,10 @@ EXTENDS Naturals -\* Maximum height of the chain where this module is running. -CONSTANT MaxHeight + +CONSTANTS MaxHeight, \* Maximum height of the chain where this module runs. + ConnectionIDs,\* The set of all possible connection IDs. + ClientIDs \* The set of all possible client IDs. VARIABLES @@ -15,16 +17,6 @@ VARIABLES ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} -(* TODO: constants *) -ConnectionIDs == {"connAtoB", "connBtoA"} -nullConnectionID == "null" - - -(* TODO: constants *) -ClientIDs == { "clientA", "clientB" } -nullClientID == "null" - - ConnectionEnds == [ connectionID : ConnectionIDs, @@ -56,7 +48,7 @@ Clients == consensusState : Heights ] -nullClient == [clientID |-> nullClientID] +nullClient == "no client" (******************************** Messages ******************************** These messages are connection handshake specific. @@ -77,21 +69,11 @@ ConnectionHandshakeMessages == [type : {"CHMsgTry"}, parameters : ConnectionParameters -\* stateProof : Proofs, +\* stateProof : Proofs, \* consensusHeight : Proofs ] NoMsg == [ type |-> "none" ] - - -(* The initialization message that this module expects *) -InitMsg == - CHOOSE m \in ConnectionHandshakeMessages : - /\ m.type = "CHMsgInit" - /\ m.parameters.localEnd.connectionID /= m.parameters.remoteEnd.connectionID - /\ m.parameters.localEnd.clientID /= m.parameters.remoteEnd.clientID -\* [type : { "CHMsgInit" }, -\* parameters : ConnectionParameters] (*************************************************************************** @@ -103,12 +85,21 @@ InitMsg == \* If the connection in the store is `nullConnection`, returns true. \* Otherwise, returns true if `para` matches the connection in the store, and false otherwise. ValidConnectionParameters(para) == - \/ store.connection = nullConnection - \/ /\ store.connection /= nullConnection - /\ store.connection.parameters.localEnd.connectionID = para.localEnd.connectionID - /\ store.connection.parameters.remoteEnd.connectionID = para.remoteEnd.connectionID - /\ store.connection.parameters.localEnd.clientID = para.localEnd.clientID - /\ store.connection.parameters.remoteEnd.clientID = para.remoteEnd.clientID + /\ para.localEnd.connectionID \in ConnectionIDs + /\ para.localEnd.clientID \in ClientIDs + /\ \/ store.connection = nullConnection + \/ /\ store.connection /= nullConnection + /\ store.connection.parameters.localEnd.connectionID = para.localEnd.connectionID + /\ store.connection.parameters.remoteEnd.connectionID = para.remoteEnd.connectionID + /\ store.connection.parameters.localEnd.clientID = para.localEnd.clientID + /\ store.connection.parameters.remoteEnd.clientID = para.remoteEnd.clientID + +(* The initialization message that this module expects *) +InitMsg == + CHOOSE m \in ConnectionHandshakeMessages : + /\ m.type = "CHMsgInit" + /\ m.parameters.localEnd.connectionID \in ConnectionIDs + /\ m.parameters.localEnd.clientID \in ClientIDs \* Given a ConnectionParameters record `para`, this operator returns a new set @@ -200,6 +191,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Mon May 04 16:12:27 CEST 2020 by adi +\* Last modified Tue May 05 13:30:21 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 9d4ba5369a..e8132afcd6 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -14,6 +14,26 @@ VARIABLES stillMalicious \* If TRUE, environment interferes w/ CH protocol. +chainAParameters == [ + connectionID |-> { "connAtoB" }, + clientID |-> { "clientChainA" } +] + +chainBParameters == [ + connectionID |-> { "connBtoA" }, + clientID |-> { "clientChainB" } +] + + +\*chainAConnectionID = "connAtoB" +\*chainBConnectionID = "connBtoA" +\*chainAClientID = "clientChainA" +\*chainBClientID = "clientChainA" + + +ClientIDs == { chainAParameters.clientID, chainBParameters.clientID } +ConnectionIDs == { chainAParameters.connectionID, chainBParameters.connectionID } + chainAVars == <> chainBVars == <> chainStoreVars == <> @@ -21,17 +41,22 @@ allVars == <> chmA == INSTANCE ConnectionHandshakeModule - WITH MaxHeight <- MaxHeight, - inMsg <- bufChainA, - outMsg <- bufChainB, - store <- storeChainA + WITH MaxHeight <- MaxHeight, + inMsg <- bufChainA, + outMsg <- bufChainB, + store <- storeChainA, + ConnectionIDs <- chainAParameters.connectionID, + ClientIDs <- chainAParameters.clientID + chmB == INSTANCE ConnectionHandshakeModule - WITH MaxHeight <- MaxHeight, - inMsg <- bufChainB, \* Flip the message buffers w.r.t. chain A buffers. - outMsg <- bufChainA, \* Inbound for "A" is outbound for "B". - store <- storeChainB + WITH MaxHeight <- MaxHeight, + inMsg <- bufChainB, \* Flip the message buffers w.r.t. chain A buffers. + outMsg <- bufChainA, \* Inbound for "A" is outbound for "B". + store <- storeChainB, + ConnectionIDs <- chainBParameters.connectionID, + ClientIDs <- chainBParameters.clientID (*************************************************************************** @@ -45,6 +70,11 @@ InitEnv == \/ bufChainB = chmB!InitMsg /\ bufChainA = chmA!NoMsg +(* The environment overwrites the buffer of one of the chains. + This interferes with the CH protocol in two ways: + 1. by introducing additional messages that are incorrect, + 2. by dropping correct messages (overwritting them). + *) MaliciousNextEnv == \/ /\ bufChainA' \in chmA!ConnectionHandshakeMessages /\ UNCHANGED bufChainB @@ -59,7 +89,7 @@ NextEnv == \/ /\ stillMalicious /\ stillMalicious' = FALSE /\ UNCHANGED<> - + CHDone == /\ storeChainA.connection.state = "INIT" (* TODO: should be OPEN *) @@ -119,6 +149,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Mon May 04 16:08:21 CEST 2020 by adi +\* Last modified Tue May 05 13:27:23 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From f8a9653b4825bb0ca6071ec3b3cb9883dfefc451 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 5 May 2020 16:35:54 +0200 Subject: [PATCH 30/63] ConsistencyInv holds. Better code structure --- .../ConnectionHandshakeModule.tla | 46 ++----------- .../spec/connection-handshake/Environment.tla | 66 +++++++++++++++---- 2 files changed, 57 insertions(+), 55 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 2bb8f8bb5e..e2c59cc30c 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -14,9 +14,6 @@ VARIABLES store \* The local store of the chain running this chModule. -ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} - - ConnectionEnds == [ connectionID : ConnectionIDs, @@ -24,17 +21,7 @@ ConnectionEnds == \* commitmentPrefix add later ] -ConnectionParameters == - [ - localEnd : ConnectionEnds, - remoteEnd : ConnectionEnds - ] -Connections == - [ - parameters : ConnectionParameters, - state : ConnectionStates - ] nullConnection == [state |-> "UNINIT"] @@ -50,31 +37,9 @@ Clients == nullClient == "no client" -(******************************** Messages ******************************** - These messages are connection handshake specific. - - In the low-level connection handshake protocol, the four messages have the - following types: ConnOpenInit, ConnOpenTry, ConnOpenAck, ConnOpenConfirm. - These are described in ICS 003. - In this high-level specification, we choose slightly different names, to - make an explicit distinction to the low-level protocol. Message types - are as follows: CHMsgInit, CHMsgTry, CHMsgAck, and CHMsgConfirm. Notice that - the fields of each message are also different to the ICS 003 specification. - ***************************************************************************) -ConnectionHandshakeMessages == - [type : {"CHMsgInit"}, - parameters : ConnectionParameters] - - \union - - [type : {"CHMsgTry"}, - parameters : ConnectionParameters -\* stateProof : Proofs, -\* consensusHeight : Proofs - ] NoMsg == [ type |-> "none" ] - + (*************************************************************************** Helper operators. @@ -95,11 +60,8 @@ ValidConnectionParameters(para) == /\ store.connection.parameters.remoteEnd.clientID = para.remoteEnd.clientID (* The initialization message that this module expects *) -InitMsg == - CHOOSE m \in ConnectionHandshakeMessages : - /\ m.type = "CHMsgInit" - /\ m.parameters.localEnd.connectionID \in ConnectionIDs - /\ m.parameters.localEnd.clientID \in ClientIDs +ChooseLocalEnd == + CHOOSE l \in ConnectionEnds : TRUE \* Given a ConnectionParameters record `para`, this operator returns a new set @@ -191,6 +153,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Tue May 05 13:30:21 CEST 2020 by adi +\* Last modified Tue May 05 16:30:32 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index e8132afcd6..82030b1782 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -25,12 +25,6 @@ chainBParameters == [ ] -\*chainAConnectionID = "connAtoB" -\*chainBConnectionID = "connBtoA" -\*chainAClientID = "clientChainA" -\*chainBClientID = "clientChainA" - - ClientIDs == { chainAParameters.clientID, chainBParameters.clientID } ConnectionIDs == { chainAParameters.connectionID, chainBParameters.connectionID } @@ -59,15 +53,61 @@ chmB == INSTANCE ConnectionHandshakeModule ClientIDs <- chainBParameters.clientID +ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} + + +ConnectionParameters == + [ + localEnd : chmA!ConnectionEnds, + remoteEnd : chmB!ConnectionEnds + ] + +Connections == + [ + parameters : ConnectionParameters, + state : ConnectionStates + ] + +(******************************** Messages ******************************** + These messages are connection handshake specific. + + In the low-level connection handshake protocol, the four messages have the + following types: ConnOpenInit, ConnOpenTry, ConnOpenAck, ConnOpenConfirm. + These are described in ICS 003. + In this high-level specification, we choose slightly different names, to + make an explicit distinction to the low-level protocol. Message types + are as follows: CHMsgInit, CHMsgTry, CHMsgAck, and CHMsgConfirm. Notice that + the fields of each message are also different to the ICS 003 specification. + ***************************************************************************) +ConnectionHandshakeMessages == + [type : {"CHMsgInit"}, + parameters : ConnectionParameters] + + \union + + [type : {"CHMsgTry"}, + parameters : ConnectionParameters +\* stateProof : Proofs, +\* consensusHeight : Proofs + ] + + (*************************************************************************** Environment actions. ***************************************************************************) +InitMsg(le, re) == + [type |-> "CHMsgInit", + parameters |-> [localEnd |-> le, + remoteEnd |-> re]] + InitEnv == /\ stillMalicious = TRUE - /\ \/ bufChainA = chmA!InitMsg /\ bufChainB = chmB!NoMsg - \/ bufChainB = chmB!InitMsg /\ bufChainA = chmA!NoMsg + /\ \/ /\ bufChainA = InitMsg(chmA!ChooseLocalEnd, chmB!ChooseLocalEnd) + /\ bufChainB = chmB!NoMsg + \/ /\ bufChainB = InitMsg(chmB!ChooseLocalEnd, chmA!ChooseLocalEnd) + /\ bufChainA = chmA!NoMsg (* The environment overwrites the buffer of one of the chains. @@ -76,9 +116,9 @@ InitEnv == 2. by dropping correct messages (overwritting them). *) MaliciousNextEnv == - \/ /\ bufChainA' \in chmA!ConnectionHandshakeMessages + \/ /\ bufChainA' \in ConnectionHandshakeMessages /\ UNCHANGED bufChainB - \/ /\ bufChainB' \in chmB!ConnectionHandshakeMessages + \/ /\ bufChainB' \in ConnectionHandshakeMessages /\ UNCHANGED bufChainA @@ -124,8 +164,8 @@ Spec == TypeInvariant == - /\ bufChainA \in chmA!ConnectionHandshakeMessages - /\ bufChainB \in chmB!ConnectionHandshakeMessages + /\ bufChainA \in ConnectionHandshakeMessages + /\ bufChainB \in ConnectionHandshakeMessages \* Liveness property. @@ -149,6 +189,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Tue May 05 13:27:23 CEST 2020 by adi +\* Last modified Tue May 05 16:34:19 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 685a839a4deae6661fb456caf072949240e0ee39 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 5 May 2020 18:35:21 +0200 Subject: [PATCH 31/63] Switching to sequences for buffers (instead of a single location). --- .../ConnectionHandshakeModule.tla | 69 ++++++++++--------- .../spec/connection-handshake/Environment.tla | 35 ++++++---- 2 files changed, 57 insertions(+), 47 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index e2c59cc30c..308047717b 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -1,16 +1,21 @@ --------------------- MODULE ConnectionHandshakeModule --------------------- -EXTENDS Naturals +EXTENDS Naturals, FiniteSets, Sequences CONSTANTS MaxHeight, \* Maximum height of the chain where this module runs. ConnectionIDs,\* The set of all possible connection IDs. - ClientIDs \* The set of all possible client IDs. + ClientIDs, \* The set of all possible client IDs. + MaxBufLen \* Maximum length of the input and output buffers. + + +ASSUME Cardinality(ConnectionIDs) >= 1 +ASSUME Cardinality(ClientIDs) >= 1 VARIABLES - inMsg, \* A buffer holding any message incoming to the chModule. - outMsg, \* A buffer holding any message outbound from the chModule. + inBuf, \* A buffer (Sequence) holding any message(s) incoming to the chModule. + outBuf, \* A buffer (Sequence) holding outbound message(s) from the chModule. store \* The local store of the chain running this chModule. @@ -22,7 +27,6 @@ ConnectionEnds == ] - nullConnection == [state |-> "UNINIT"] @@ -85,9 +89,9 @@ HandleInitMsg(m) == oMsg |-> [parameters |-> FlipConnectionParameters(m.parameters), type |-> "CHMsgTry"]] ELSE [nConnection |-> store.connection, - oMsg |-> NoMsg] + oMsg |-> NoMsg] IN /\ store' = [store EXCEPT !.connection = res.nConnection] - /\ outMsg' = res.oMsg + /\ outBuf' = Append(outBuf, res.oMsg) HandleTryMsg(m) == @@ -100,59 +104,60 @@ HandleTryMsg(m) == ELSE [nConnection |-> store.connection, oMsg |-> NoMsg] IN /\ store' = [store EXCEPT !.connection = res.nConnection] - /\ outMsg' = res.oMsg + /\ outBuf' = res.oMsg \* If MaxHeight is not yet reached, then advance the height of the chain. -AdvanceChainHeight == - /\ store.height < MaxHeight - /\ store' = [store EXCEPT !.height = @ + 1] - /\ UNCHANGED <> +\*AdvanceChainHeight == +\* /\ store.height < MaxHeight +\* /\ store' = [store EXCEPT !.height = @ + 1] +\* /\ UNCHANGED <> (****** Expects a valid ConnectionHandshakeMessage record. Does two basic actions: 1. Updates the chain store, and - 2. Updates outMsg with a reply message, possibly NoMsg. + 2. Updates outBuf with a reply message, possibly NoMsg. *****) -ProcessConnectionHandshakeMessage(msg) == +ProcessConnectionHandshakeMsg(msg) == \/ msg.type = "CHMsgInit" /\ HandleInitMsg(msg) \/ msg.type = "CHMsgTry" /\ HandleTryMsg(msg) \* Generic handle for any type of inbound message. -\* Assumes that 'inMsg' is not empty. -\* Takes care of changing the 'store' and 'outMsg'. -ProcessInMsg == - /\ IF ValidConnectionParameters(inMsg.parameters) = TRUE - THEN ProcessConnectionHandshakeMessage(inMsg) - \* The connection parameters are not valid. No state transition. - ELSE /\ outMsg' = NoMsg \* No reply. - /\ UNCHANGED store - /\ inMsg' = NoMsg \* Flush the inbound message buffer. +\* Expects a parameter a non-null message. +\* Takes care of changing the 'store' and enqueing any reply msg in 'outBuf'. +ProcessMsg(m) == + IF ValidConnectionParameters(m.parameters) = TRUE + THEN ProcessConnectionHandshakeMsg(m) + \* The connection parameters are not valid. No state transition. + ELSE UNCHANGED<> +(* TODO: structure this ^^ logic better using LET .. IN *) (*************************************************************************** Connection Handshake Module main spec. ***************************************************************************) + Init(chainID) == - store = [id |-> chainID, - height |-> 1, - connection |-> nullConnection, - client |-> nullClient] + /\ store = [id |-> chainID, + height |-> 1, + connection |-> nullConnection, + client |-> nullClient] Next == - IF inMsg /= NoMsg - THEN ProcessInMsg - \* We have no input message, nothing for us to do. - ELSE UNCHANGED <> + LET m == Head(inBuf) + IN /\ inBuf /= <<>> \* Enabled if we have a message in our inbound buffer. + /\ Len(outBuf) < MaxBufLen + /\ ProcessMsg(m) + /\ inBuf' = Tail(inBuf) \* Remove the head of the inbound message buffer. ============================================================================= \* Modification History -\* Last modified Tue May 05 16:30:32 CEST 2020 by adi +\* Last modified Tue May 05 18:34:29 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 82030b1782..a3eb4ff490 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -1,13 +1,17 @@ ---------------------------- MODULE Environment ---------------------------- -EXTENDS Naturals, FiniteSets +EXTENDS Naturals, FiniteSets, Sequences -CONSTANT MaxHeight \* Maximum height of any chain in the system. +CONSTANT MaxHeight, \* Maximum height of any chain in the system. + MaxBufLen \* Length (size) of message buffers. + +ASSUME MaxHeight > 1 +ASSUME MaxBufLen > 1 VARIABLES - bufChainA, \* A buffer for messages inbound to chain A. + bufChainA, \* A buffer (sequence) for messages inbound to chain A. bufChainB, \* A buffer for messages inbound to chain B. storeChainA, \* The local store of chain A. storeChainB, \* The local store of chain B. @@ -36,8 +40,8 @@ allVars == <> chmA == INSTANCE ConnectionHandshakeModule WITH MaxHeight <- MaxHeight, - inMsg <- bufChainA, - outMsg <- bufChainB, + inBuf <- bufChainA, + outBuf <- bufChainB, store <- storeChainA, ConnectionIDs <- chainAParameters.connectionID, ClientIDs <- chainAParameters.clientID @@ -46,8 +50,8 @@ chmA == INSTANCE ConnectionHandshakeModule chmB == INSTANCE ConnectionHandshakeModule WITH MaxHeight <- MaxHeight, - inMsg <- bufChainB, \* Flip the message buffers w.r.t. chain A buffers. - outMsg <- bufChainA, \* Inbound for "A" is outbound for "B". + inBuf <- bufChainB, \* Flip the message buffers w.r.t. chain A buffers. + outBuf <- bufChainA, \* Inbound for "A" is outbound for "B". store <- storeChainB, ConnectionIDs <- chainBParameters.connectionID, ClientIDs <- chainBParameters.clientID @@ -104,10 +108,10 @@ InitMsg(le, re) == InitEnv == /\ stillMalicious = TRUE - /\ \/ /\ bufChainA = InitMsg(chmA!ChooseLocalEnd, chmB!ChooseLocalEnd) - /\ bufChainB = chmB!NoMsg - \/ /\ bufChainB = InitMsg(chmB!ChooseLocalEnd, chmA!ChooseLocalEnd) - /\ bufChainA = chmA!NoMsg + /\ \/ /\ bufChainA = <> + /\ bufChainB = <<>> \* Empty buffer. + \/ /\ bufChainB = <> + /\ bufChainA = <<>> (* The environment overwrites the buffer of one of the chains. @@ -123,10 +127,10 @@ MaliciousNextEnv == NextEnv == - \/ /\ stillMalicious + \/ /\ stillMalicious = TRUE /\ MaliciousNextEnv /\ UNCHANGED stillMalicious - \/ /\ stillMalicious + \/ /\ stillMalicious = TRUE /\ stillMalicious' = FALSE /\ UNCHANGED<> @@ -143,9 +147,10 @@ CHDone == Init == - /\ InitEnv /\ chmA!Init("chainA") /\ chmB!Init("chainB") + /\ InitEnv + \* The two CH modules and the environment alternate their steps. @@ -189,6 +194,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Tue May 05 16:34:19 CEST 2020 by adi +\* Last modified Tue May 05 18:29:38 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 739303828dd3b8be04af778192dddfb35d07e3d3 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 6 May 2020 13:46:04 +0200 Subject: [PATCH 32/63] Better sequence-based buffers --- .../ConnectionHandshakeModule.tla | 8 +--- .../spec/connection-handshake/Environment.tla | 43 +++++++++++++------ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 308047717b..2f34e895d9 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -63,10 +63,6 @@ ValidConnectionParameters(para) == /\ store.connection.parameters.localEnd.clientID = para.localEnd.clientID /\ store.connection.parameters.remoteEnd.clientID = para.remoteEnd.clientID -(* The initialization message that this module expects *) -ChooseLocalEnd == - CHOOSE l \in ConnectionEnds : TRUE - \* Given a ConnectionParameters record `para`, this operator returns a new set \* of parameters where the local and remote ends are flipped (i.e., reversed). @@ -104,7 +100,7 @@ HandleTryMsg(m) == ELSE [nConnection |-> store.connection, oMsg |-> NoMsg] IN /\ store' = [store EXCEPT !.connection = res.nConnection] - /\ outBuf' = res.oMsg + /\ outBuf' = Append(outBuf, res.oMsg) \* If MaxHeight is not yet reached, then advance the height of the chain. @@ -158,6 +154,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Tue May 05 18:34:29 CEST 2020 by adi +\* Last modified Wed May 06 13:35:35 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index a3eb4ff490..5295de5d0f 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -1,6 +1,6 @@ ---------------------------- MODULE Environment ---------------------------- -EXTENDS Naturals, FiniteSets, Sequences +EXTENDS Naturals, FiniteSets, Sequences, TLC CONSTANT MaxHeight, \* Maximum height of any chain in the system. @@ -45,7 +45,6 @@ chmA == INSTANCE ConnectionHandshakeModule store <- storeChainA, ConnectionIDs <- chainAParameters.connectionID, ClientIDs <- chainAParameters.clientID - chmB == INSTANCE ConnectionHandshakeModule @@ -96,21 +95,36 @@ ConnectionHandshakeMessages == ] +(* The set of all Init messages, such that the local end is the + set 'le', and the remote end is set 're'. *) +InitMsgs(le, re) == + [type : {"CHMsgInit"}, + parameters : [localEnd : le, + remoteEnd : re]] + + +(* TODO: find a more elegant way to do this. + What we want, ideally, is to specify this: + z \in x + to obtain a random Sequence z out of a set of sequences x, obtaining: + bufChainA \in InitMsgs + instead of the awkward lines for building bufChainA and bufChainB below. + *) +ChooseInitMsg(le, re) == + CHOOSE x \in InitMsgs(le, re) : TRUE + + (*************************************************************************** Environment actions. ***************************************************************************) -InitMsg(le, re) == - [type |-> "CHMsgInit", - parameters |-> [localEnd |-> le, - remoteEnd |-> re]] - InitEnv == /\ stillMalicious = TRUE - /\ \/ /\ bufChainA = <> + (* Assign a sequence (a function) with 1 element -- the Init msg -- to bufChainA. *) + /\ \/ /\ bufChainA = [x \in {1} |-> ChooseInitMsg(chmA!ConnectionEnds, chmB!ConnectionEnds)] /\ bufChainB = <<>> \* Empty buffer. - \/ /\ bufChainB = <> + \/ /\ bufChainB = [x \in {1} |-> ChooseInitMsg(chmB!ConnectionEnds, chmA!ConnectionEnds)] /\ bufChainA = <<>> @@ -120,10 +134,11 @@ InitEnv == 2. by dropping correct messages (overwritting them). *) MaliciousNextEnv == - \/ /\ bufChainA' \in ConnectionHandshakeMessages - /\ UNCHANGED bufChainB - \/ /\ bufChainB' \in ConnectionHandshakeMessages - /\ UNCHANGED bufChainA + LET arbitraryMsg == CHOOSE x \in ConnectionHandshakeMessages : TRUE (* TODO *) + IN \/ /\ bufChainA' = Append(bufChainA, arbitraryMsg) + /\ UNCHANGED bufChainB + \/ /\ bufChainB' = Append(bufChainB, arbitraryMsg) + /\ UNCHANGED bufChainA NextEnv == @@ -194,6 +209,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Tue May 05 18:29:38 CEST 2020 by adi +\* Last modified Wed May 06 13:43:35 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 295c0fa2d715a972f1ef519f7464e3d8cea6694a Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 6 May 2020 17:03:08 +0200 Subject: [PATCH 33/63] Fixed CHOOSE todos; more idiomatic handlers --- .../ConnectionHandshakeModule.tla | 153 +++++++++++------- .../spec/connection-handshake/Environment.tla | 45 ++++-- 2 files changed, 126 insertions(+), 72 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 2f34e895d9..302a18c6d4 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -42,26 +42,35 @@ Clients == nullClient == "no client" -NoMsg == [ type |-> "none" ] - - (*************************************************************************** Helper operators. ***************************************************************************) - -\* Validates a ConnectionParameter `para` against an already existing connection. -\* If the connection in the store is `nullConnection`, returns true. -\* Otherwise, returns true if `para` matches the connection in the store, and false otherwise. +(* Returns true if 'para' matches the parameters in the local connection, + and returns false otherwise. + *) +CheckLocalParameters(para) == + /\ store.connection.parameters.localEnd.connectionID = para.localEnd.connectionID + /\ store.connection.parameters.remoteEnd.connectionID = para.remoteEnd.connectionID + /\ store.connection.parameters.localEnd.clientID = para.localEnd.clientID + /\ store.connection.parameters.remoteEnd.clientID = para.remoteEnd.clientID + + +(* Validates a ConnectionParameter. + Expects as input a ConnectionParameter 'para' and returns true or false. + + This is a basic validation step, making sure that 'para' is valid with + respect to module-level constants ConnectionIDs and ClientIDs. + If there is a connection in the store, this also validates 'para' + against the parameters of an existing connection by relying on the + state predicate CheckLocalParameters. +*) ValidConnectionParameters(para) == /\ para.localEnd.connectionID \in ConnectionIDs /\ para.localEnd.clientID \in ClientIDs /\ \/ store.connection = nullConnection \/ /\ store.connection /= nullConnection - /\ store.connection.parameters.localEnd.connectionID = para.localEnd.connectionID - /\ store.connection.parameters.remoteEnd.connectionID = para.remoteEnd.connectionID - /\ store.connection.parameters.localEnd.clientID = para.localEnd.clientID - /\ store.connection.parameters.remoteEnd.clientID = para.remoteEnd.clientID + /\ CheckLocalParameters(para) \* Given a ConnectionParameters record `para`, this operator returns a new set @@ -71,36 +80,76 @@ FlipConnectionParameters(para) == remoteEnd |-> para.localEnd] +(* Returns a proof, stating that the local store on this chain comprises a + connection in a certain state. + *) +GetStateProof == + [content |-> "state proof content"] + + +(* TODO *) +GetClientProof == + [content |-> "client proof content"] + + (*************************************************************************** Connection Handshake Module actions & operators. ***************************************************************************) -\* Handles a `CHMsgInit` message. +(* Handles a "CHMsgInit" message 'm'. + + Primes the store.connection to become initialized with the parameters + specified in 'm'. Also creates a reply message, enqueued on the outgoing + buffer. + *) HandleInitMsg(m) == - (* TODO: add proofs in the THEN branch. *) - LET res == IF store.connection.state = "UNINIT" - THEN [nConnection |-> [parameters |-> m.parameters, - state |-> "INIT"], - oMsg |-> [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgTry"]] - ELSE [nConnection |-> store.connection, - oMsg |-> NoMsg] - IN /\ store' = [store EXCEPT !.connection = res.nConnection] - /\ outBuf' = Append(outBuf, res.oMsg) - - + /\ store.connection.state = "UNINIT" + /\ Len(outBuf) < MaxBufLen \* Enabled if we can reply to the remote chain. + /\ LET newCon == [parameters |-> m.parameters, + state |-> "INIT"] + sProof == GetStateProof + cProof == GetClientProof + replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgTry", + stateProof |-> sProof, + clientProof |-> cProof] + IN /\ outBuf' = Append(outBuf, replyMsg) + /\ store' = [store EXCEPT !.connection = newCon] + + +(* Handles a "CHMsgTry" message. + *) HandleTryMsg(m) == - (* TODO: add proofs & more logic. *) - LET res == IF store.connection.state = "UNINIT" - THEN [nConnection |-> [parameters |-> m.parameters, - state |-> "INIT"], - oMsg |-> [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgAck"]] - ELSE [nConnection |-> store.connection, - oMsg |-> NoMsg] - IN /\ store' = [store EXCEPT !.connection = res.nConnection] - /\ outBuf' = Append(outBuf, res.oMsg) + /\ \/ store.connection.state = "UNINIT" + \/ store.connection.state = "INIT" /\ CheckLocalParameters(m.parameters) + /\ Len(outBuf) < MaxBufLen + /\ LET newCon == [parameters |-> m.parameters, + state |-> "INIT"] + sProof == GetStateProof + cProof == GetClientProof + replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgAck"] + IN /\ outBuf' = Append(outBuf, replyMsg) + /\ store' = [store EXCEPT !.connection = newCon] + + +(* Handles a "CHMsgAck" message. + *) +HandleAckMsg(m) == + /\ \/ store.connection.state = "INIT" + \/ store.connection.state = "TRYOPEN" + /\ CheckLocalParameters(m.parameters) + /\ Len(outBuf) < MaxBufLen + /\ LET newCon == [parameters |-> m.parameters, + state |-> "INIT"] + sProof == GetStateProof + cProof == GetClientProof + replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgAck"] + IN /\ outBuf' = Append(outBuf, replyMsg) + /\ store' = [store EXCEPT !.connection = newCon] + \* If MaxHeight is not yet reached, then advance the height of the chain. @@ -110,26 +159,15 @@ HandleTryMsg(m) == \* /\ UNCHANGED <> -(****** - Expects a valid ConnectionHandshakeMessage record. - Does two basic actions: - 1. Updates the chain store, and - 2. Updates outBuf with a reply message, possibly NoMsg. - *****) -ProcessConnectionHandshakeMsg(msg) == - \/ msg.type = "CHMsgInit" /\ HandleInitMsg(msg) - \/ msg.type = "CHMsgTry" /\ HandleTryMsg(msg) - - -\* Generic handle for any type of inbound message. -\* Expects a parameter a non-null message. -\* Takes care of changing the 'store' and enqueing any reply msg in 'outBuf'. +(* Generic action for handling any type of inbound message. + Expects as parameter a message. + Takes care of invoking priming the 'store' and any reply msg in 'outBuf'. + *) ProcessMsg(m) == - IF ValidConnectionParameters(m.parameters) = TRUE - THEN ProcessConnectionHandshakeMsg(m) - \* The connection parameters are not valid. No state transition. - ELSE UNCHANGED<> -(* TODO: structure this ^^ logic better using LET .. IN *) + /\ ValidConnectionParameters(m.parameters) = TRUE + /\ \/ m.type = "CHMsgInit" /\ HandleInitMsg(m) + \/ m.type = "CHMsgTry" /\ HandleTryMsg(m) + \/ m.type = "CHMsgAck" /\ HandleAckMsg(m) (*************************************************************************** @@ -146,14 +184,13 @@ Init(chainID) == Next == LET m == Head(inBuf) - IN /\ inBuf /= <<>> \* Enabled if we have a message in our inbound buffer. - /\ Len(outBuf) < MaxBufLen - /\ ProcessMsg(m) - /\ inBuf' = Tail(inBuf) \* Remove the head of the inbound message buffer. + IN /\ inBuf /= <<>> \* Enabled if we have an inbound msg. + /\ ProcessMsg(m) \* Generic action for handling a msg. + /\ inBuf' = Tail(inBuf) \* Strip the head of our inbound msg. buffer. ============================================================================= \* Modification History -\* Last modified Wed May 06 13:35:35 CEST 2020 by adi +\* Last modified Wed May 06 16:44:28 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 5295de5d0f..68bd078ebd 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -71,6 +71,11 @@ Connections == state : ConnectionStates ] +Proofs == + [ + height : 1..MaxHeight + ] + (******************************** Messages ******************************** These messages are connection handshake specific. @@ -85,14 +90,20 @@ Connections == ConnectionHandshakeMessages == [type : {"CHMsgInit"}, parameters : ConnectionParameters] - \union - [type : {"CHMsgTry"}, - parameters : ConnectionParameters -\* stateProof : Proofs, -\* consensusHeight : Proofs - ] + parameters : ConnectionParameters, + stateProof : Proofs, + clientProof : Proofs] + \union + [type : {"CHMsgAck"}, + parameters : ConnectionParameters, + stateProof : Proofs, + clientProof : Proofs] + \union + [type : {"CHMsgConfirm"}, + parameters : ConnectionParameters, + stateProof : Proofs] (* The set of all Init messages, such that the local end is the @@ -122,9 +133,9 @@ ChooseInitMsg(le, re) == InitEnv == /\ stillMalicious = TRUE (* Assign a sequence (a function) with 1 element -- the Init msg -- to bufChainA. *) - /\ \/ /\ bufChainA = [x \in {1} |-> ChooseInitMsg(chmA!ConnectionEnds, chmB!ConnectionEnds)] + /\ \/ /\ bufChainA \in {<> : msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} /\ bufChainB = <<>> \* Empty buffer. - \/ /\ bufChainB = [x \in {1} |-> ChooseInitMsg(chmB!ConnectionEnds, chmA!ConnectionEnds)] + \/ /\ bufChainB \in {<> : msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} /\ bufChainA = <<>> @@ -134,11 +145,17 @@ InitEnv == 2. by dropping correct messages (overwritting them). *) MaliciousNextEnv == - LET arbitraryMsg == CHOOSE x \in ConnectionHandshakeMessages : TRUE (* TODO *) - IN \/ /\ bufChainA' = Append(bufChainA, arbitraryMsg) - /\ UNCHANGED bufChainB - \/ /\ bufChainB' = Append(bufChainB, arbitraryMsg) - /\ UNCHANGED bufChainA + (* Without the first constraint, Env could fill buffers (DoS attack). *) + \/ /\ Len(bufChainA) < MaxBufLen - 1 + /\ bufChainA' \in + {Append(bufChainA, arbitraryMsg) : + arbitraryMsg \in ConnectionHandshakeMessages} + /\ UNCHANGED bufChainB + \/ /\ Len(bufChainA) < MaxBufLen - 1 + /\ bufChainB' \in + {Append(bufChainB, arbitraryMsg) : + arbitraryMsg \in ConnectionHandshakeMessages} + /\ UNCHANGED bufChainA NextEnv == @@ -209,6 +226,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Wed May 06 13:43:35 CEST 2020 by adi +\* Last modified Wed May 06 16:41:45 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From c89803ba31a78875273f06b7e1e0237b49faa841 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 7 May 2020 15:17:38 +0200 Subject: [PATCH 34/63] Found generic solution for invoking msg. handlers --- .../ConnectionHandshakeModule.tla | 63 ++++++++++++++----- .../spec/connection-handshake/Environment.tla | 38 +++++------ 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 302a18c6d4..ddce1e108b 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -1,12 +1,12 @@ --------------------- MODULE ConnectionHandshakeModule --------------------- -EXTENDS Naturals, FiniteSets, Sequences +EXTENDS Naturals, FiniteSets, Sequences, TLC -CONSTANTS MaxHeight, \* Maximum height of the chain where this module runs. - ConnectionIDs,\* The set of all possible connection IDs. - ClientIDs, \* The set of all possible client IDs. - MaxBufLen \* Maximum length of the input and output buffers. +CONSTANTS MaxHeight, \* Maximum height of the local chain. + ConnectionIDs, \* The set of all possible connection IDs. + ClientIDs, \* The set of all possible client IDs. + MaxBufLen \* Maximum length of the input and output buffers. ASSUME Cardinality(ConnectionIDs) >= 1 @@ -42,6 +42,15 @@ Clients == nullClient == "no client" +(* The set of all types which a message can take. + + TODO: These types are also used in the Environment module. + Is it possible to restrict these to a single module? + *) +CHMessageTypes == + { "CHMsgInit", "CHMsgTry", "CHMsgAck", "CHMsgConfirm"} + + (*************************************************************************** Helper operators. ***************************************************************************) @@ -73,6 +82,10 @@ ValidConnectionParameters(para) == /\ CheckLocalParameters(para) +ValidMessageType(type) == + /\ type \in CHMessageTypes + + \* Given a ConnectionParameters record `para`, this operator returns a new set \* of parameters where the local and remote ends are flipped (i.e., reversed). FlipConnectionParameters(para) == @@ -97,6 +110,17 @@ GetClientProof == ***************************************************************************) +(* Drops a message without any priming of local variables. + This action always enables (returns true); use with care. + This action is analogous to "abortTransaction" function from ICS specs. + *) +DropMsg(m) == + PrintT([cause |-> "The msg. was not valid, dropping", + msg |-> m, + chain |-> store.id]) + /\ UNCHANGED<> + + (* Handles a "CHMsgInit" message 'm'. Primes the store.connection to become initialized with the parameters @@ -121,6 +145,7 @@ HandleInitMsg(m) == (* Handles a "CHMsgTry" message. *) HandleTryMsg(m) == + (* The good-case path. *) /\ \/ store.connection.state = "UNINIT" \/ store.connection.state = "INIT" /\ CheckLocalParameters(m.parameters) /\ Len(outBuf) < MaxBufLen @@ -151,12 +176,17 @@ HandleAckMsg(m) == /\ store' = [store EXCEPT !.connection = newCon] +(* Handles a "CHMsgConfirm" message. + *) +HandleConfirmMsg(m) == + UNCHANGED<> + \* If MaxHeight is not yet reached, then advance the height of the chain. -\*AdvanceChainHeight == -\* /\ store.height < MaxHeight -\* /\ store' = [store EXCEPT !.height = @ + 1] -\* /\ UNCHANGED <> +AdvanceChainHeight == + /\ store.height < MaxHeight + /\ store' = [store EXCEPT !.height = @ + 1] + /\ UNCHANGED <> (* Generic action for handling any type of inbound message. @@ -165,9 +195,13 @@ HandleAckMsg(m) == *) ProcessMsg(m) == /\ ValidConnectionParameters(m.parameters) = TRUE + /\ ValidMessageType(m.type) = TRUE + (* One of the following disjunctions will always enable, since + the message type is guaranteed to be valid in the THEN branch. *) /\ \/ m.type = "CHMsgInit" /\ HandleInitMsg(m) - \/ m.type = "CHMsgTry" /\ HandleTryMsg(m) - \/ m.type = "CHMsgAck" /\ HandleAckMsg(m) + \/ m.type = "CHMsgTry" /\ HandleTryMsg(m) + \/ m.type = "CHMsgAck" /\ HandleAckMsg(m) + \/ m.type = "CHMsgConfirm" /\ HandleConfirmMsg(m) (*************************************************************************** @@ -185,12 +219,13 @@ Init(chainID) == Next == LET m == Head(inBuf) IN /\ inBuf /= <<>> \* Enabled if we have an inbound msg. - /\ ProcessMsg(m) \* Generic action for handling a msg. /\ inBuf' = Tail(inBuf) \* Strip the head of our inbound msg. buffer. + /\ IF ENABLED ProcessMsg(m) + THEN ProcessMsg(m) \* Generic action for handling a msg. + ELSE DropMsg(m) ============================================================================= \* Modification History -\* Last modified Wed May 06 16:44:28 CEST 2020 by adi +\* Last modified Thu May 07 15:11:25 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi - diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 68bd078ebd..9196df7fd8 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -1,6 +1,6 @@ ---------------------------- MODULE Environment ---------------------------- -EXTENDS Naturals, FiniteSets, Sequences, TLC +EXTENDS Naturals, FiniteSets, Sequences CONSTANT MaxHeight, \* Maximum height of any chain in the system. @@ -15,7 +15,7 @@ VARIABLES bufChainB, \* A buffer for messages inbound to chain B. storeChainA, \* The local store of chain A. storeChainB, \* The local store of chain B. - stillMalicious \* If TRUE, environment interferes w/ CH protocol. + maliciousEnv \* If TRUE, environment interferes w/ CH protocol. chainAParameters == [ @@ -35,7 +35,7 @@ ConnectionIDs == { chainAParameters.connectionID, chainBParameters.connectionID chainAVars == <> chainBVars == <> chainStoreVars == <> -allVars == <> +allVars == <> chmA == INSTANCE ConnectionHandshakeModule @@ -130,11 +130,12 @@ ChooseInitMsg(le, re) == ***************************************************************************) +(* Assigns a sequence with 1 element -- the Init msg -- to either bufChainA + or bufChainB. *) InitEnv == - /\ stillMalicious = TRUE - (* Assign a sequence (a function) with 1 element -- the Init msg -- to bufChainA. *) + /\ maliciousEnv = TRUE /\ \/ /\ bufChainA \in {<> : msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} - /\ bufChainB = <<>> \* Empty buffer. + /\ bufChainB = <<>> (* Empty buffer. *) \/ /\ bufChainB \in {<> : msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} /\ bufChainA = <<>> @@ -143,11 +144,14 @@ InitEnv == This interferes with the CH protocol in two ways: 1. by introducing additional messages that are incorrect, 2. by dropping correct messages (overwritting them). + + Without the first constraint, on the "Len(bufChainA)" and "Len(bufChainB)", + Env could fill buffers (DoS attack). This can lead to a deadlock, because + chains will simply be unable to reply to each other. *) MaliciousNextEnv == - (* Without the first constraint, Env could fill buffers (DoS attack). *) \/ /\ Len(bufChainA) < MaxBufLen - 1 - /\ bufChainA' \in + /\ bufChainA' \in {Append(bufChainA, arbitraryMsg) : arbitraryMsg \in ConnectionHandshakeMessages} /\ UNCHANGED bufChainB @@ -159,11 +163,11 @@ MaliciousNextEnv == NextEnv == - \/ /\ stillMalicious = TRUE + \/ /\ maliciousEnv = TRUE /\ MaliciousNextEnv - /\ UNCHANGED stillMalicious - \/ /\ stillMalicious = TRUE - /\ stillMalicious' = FALSE + /\ UNCHANGED maliciousEnv + \/ /\ maliciousEnv = TRUE + /\ maliciousEnv' = FALSE /\ UNCHANGED<> @@ -184,13 +188,12 @@ Init == /\ InitEnv - \* The two CH modules and the environment alternate their steps. Next == \/ CHDone \/ NextEnv /\ UNCHANGED <> - \/ chmA!Next /\ UNCHANGED <> - \/ chmB!Next /\ UNCHANGED <> + \/ chmA!Next /\ UNCHANGED <> + \/ chmB!Next /\ UNCHANGED <> Spec == @@ -207,7 +210,7 @@ TypeInvariant == \* Liveness property. Termination == - <> ~ stillMalicious + <> ~ maliciousEnv => <> /\ storeChainA.connection.state = "INIT" (* TODO: should be OPEN *) /\ storeChainB.connection.state = "INIT" @@ -226,6 +229,5 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Wed May 06 16:41:45 CEST 2020 by adi +\* Last modified Thu May 07 13:36:50 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi - From b64f8d9e7df3e8e0bc43e67d569f09b5c64b9829 Mon Sep 17 00:00:00 2001 From: Anca Zamfir Date: Thu, 7 May 2020 18:02:59 +0200 Subject: [PATCH 35/63] Switch to ICS paths (#62) * Switch to ICS paths * Change "connection" to "connections" in path * Fix clippy warnings * Point tendermint-rs to master branch * Update RUSTFLAGS for nightly coverage tests Co-authored-by: Romain Ruetschi --- .github/workflows/rust.yml | 2 +- modules/Cargo.toml | 6 ++++-- modules/src/path/ics.rs | 12 ++++++------ relayer/cli/Cargo.toml | 2 +- relayer/cli/src/commands.rs | 5 +---- relayer/cli/src/commands/query/client.rs | 4 ++-- relayer/relay/Cargo.toml | 2 +- 7 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1dd88e5a42..0d8db99130 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -70,7 +70,7 @@ jobs: args: --all-features --no-fail-fast env: CARGO_INCREMENTAL: "0" - RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads" + RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=unwind -Zpanic_abort_tests" - uses: actions-rs/grcov@v0.1 - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 diff --git a/modules/Cargo.toml b/modules/Cargo.toml index b983bae85e..46321e113e 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -9,13 +9,15 @@ authors = [ [features] # Default features -default = ["paths-cosmos"] +#default = ["paths-cosmos"] +default = ["paths-ics"] # In IBC queries, use paths as defined in the Cosmos-SDK Go implementation, rather than in the ICS. paths-cosmos = [] +paths-ics = [] [dependencies] -tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "tendermint/v0.33" } +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "master" } anomaly = "0.2.0" thiserror = "1.0.11" diff --git a/modules/src/path/ics.rs b/modules/src/path/ics.rs index 8619459f1d..7157c07092 100644 --- a/modules/src/path/ics.rs +++ b/modules/src/path/ics.rs @@ -1,13 +1,13 @@ use super::{ClientStatePath, ConnectionPath, ConsensusStatePath}; -pub fn connection_path(_path: &ConnectionPath) -> String { - todo!() +pub fn connection_path(path: &ConnectionPath) -> String { + format!("connections/{}", path.connection_id) } -pub fn consensus_state_path(_path: &ConsensusStatePath) -> String { - todo!() +pub fn consensus_state_path(path: &ConsensusStatePath) -> String { + format!("clients/{}/consensusState/{}", path.client_id, path.height) } -pub fn client_state_path(_path: &ClientStatePath) -> String { - todo!() +pub fn client_state_path(path: &ClientStatePath) -> String { + format!("clients/{}/clientState", path.client_id) } diff --git a/relayer/cli/Cargo.toml b/relayer/cli/Cargo.toml index 649f55ea8a..e101a695d1 100644 --- a/relayer/cli/Cargo.toml +++ b/relayer/cli/Cargo.toml @@ -10,7 +10,7 @@ authors = [ [dependencies] relayer = { path = "../relay" } relayer-modules = { path = "../../modules" } -tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "tendermint/v0.33" } +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "master" } abscissa_tokio = "0.5.1" anomaly = "0.2.0" diff --git a/relayer/cli/src/commands.rs b/relayer/cli/src/commands.rs index 808ebc9d9e..847cd02ae5 100644 --- a/relayer/cli/src/commands.rs +++ b/relayer/cli/src/commands.rs @@ -73,9 +73,6 @@ impl Configurable for CliCmd { /// This can be safely deleted if you don't want to override config /// settings from command-line options. fn process_config(&self, config: Config) -> Result { - match self { - // CliCmd::Start(cmd) => cmd.override_config(config), - _ => Ok(config), - } + Ok(config) } } diff --git a/relayer/cli/src/commands/query/client.rs b/relayer/cli/src/commands/query/client.rs index eb61e84378..8a61373edc 100644 --- a/relayer/cli/src/commands/query/client.rs +++ b/relayer/cli/src/commands/query/client.rs @@ -50,7 +50,7 @@ impl QueryClientStateCmd { None => true, }, }; - Ok((chain_config.clone(), opts)) + Ok((chain_config, opts)) } } @@ -137,7 +137,7 @@ impl QueryClientConsensusCmd { None => true, }, }; - Ok((chain_config.clone(), opts)) + Ok((chain_config, opts)) } None => Err("missing client consensus height".to_string()), } diff --git a/relayer/relay/Cargo.toml b/relayer/relay/Cargo.toml index ce46d31f04..654d286dca 100644 --- a/relayer/relay/Cargo.toml +++ b/relayer/relay/Cargo.toml @@ -9,7 +9,7 @@ authors = [ [dependencies] relayer-modules = { path = "../../modules" } -tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "tendermint/v0.33" } +tendermint = { git = "https://github.com/informalsystems/tendermint-rs.git", branch = "master" } anomaly = "0.2.0" async-trait = "0.1.24" humantime-serde = "1.0.0" From 42a70e4849719137ed2a52d050750a3c9bd97909 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 12 May 2020 13:49:37 +0200 Subject: [PATCH 36/63] Snapshot of current state; still have bug 'In evaluation, the identifier bufChainA is either undefined or not an operator.' --- .../ConnectionHandshakeModule.tla | 27 ++++----- .../spec/connection-handshake/Environment.tla | 59 +++++++++++-------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index ddce1e108b..df3c06a7ab 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -1,6 +1,6 @@ --------------------- MODULE ConnectionHandshakeModule --------------------- -EXTENDS Naturals, FiniteSets, Sequences, TLC +EXTENDS Naturals, FiniteSets, Sequences CONSTANTS MaxHeight, \* Maximum height of the local chain. @@ -59,10 +59,12 @@ CHMessageTypes == and returns false otherwise. *) CheckLocalParameters(para) == - /\ store.connection.parameters.localEnd.connectionID = para.localEnd.connectionID - /\ store.connection.parameters.remoteEnd.connectionID = para.remoteEnd.connectionID - /\ store.connection.parameters.localEnd.clientID = para.localEnd.clientID - /\ store.connection.parameters.remoteEnd.clientID = para.remoteEnd.clientID + LET local == store.connection.parameters.localEnd + remote == store.connection.parameters.remoteEnd + IN /\ local.connectionID = para.localEnd.connectionID + /\ remote.connectionID = para.remoteEnd.connectionID + /\ local.clientID = para.localEnd.clientID + /\ remote.clientID = para.remoteEnd.clientID (* Validates a ConnectionParameter. @@ -115,10 +117,7 @@ GetClientProof == This action is analogous to "abortTransaction" function from ICS specs. *) DropMsg(m) == - PrintT([cause |-> "The msg. was not valid, dropping", - msg |-> m, - chain |-> store.id]) - /\ UNCHANGED<> + UNCHANGED<> (* Handles a "CHMsgInit" message 'm'. @@ -218,14 +217,14 @@ Init(chainID) == Next == LET m == Head(inBuf) - IN /\ inBuf /= <<>> \* Enabled if we have an inbound msg. + IN /\ inBuf # <<>> \* Enabled if we have an inbound msg. /\ inBuf' = Tail(inBuf) \* Strip the head of our inbound msg. buffer. - /\ IF ENABLED ProcessMsg(m) - THEN ProcessMsg(m) \* Generic action for handling a msg. - ELSE DropMsg(m) + /\ \/ ProcessMsg(m) \* Generic action for handling a msg. + \/ DropMsg(m) ============================================================================= \* Modification History -\* Last modified Thu May 07 15:11:25 CEST 2020 by adi +\* Last modified Tue May 12 13:23:17 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi + diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 9196df7fd8..dc92183ed2 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -32,9 +32,19 @@ chainBParameters == [ ClientIDs == { chainAParameters.clientID, chainBParameters.clientID } ConnectionIDs == { chainAParameters.connectionID, chainBParameters.connectionID } -chainAVars == <> -chainBVars == <> +(* Bundle with variables that chain A has access to. *) +chainAVars == <> (* The local chain store. *) + +(* Bundle with variables that chain B has access to. *) +chainBVars == <> (* Local chain store. *) + +(* All variables specific to chains. *) chainStoreVars == <> + allVars == <> @@ -93,17 +103,17 @@ ConnectionHandshakeMessages == \union [type : {"CHMsgTry"}, parameters : ConnectionParameters, - stateProof : Proofs, + connProof : Proofs, clientProof : Proofs] \union [type : {"CHMsgAck"}, parameters : ConnectionParameters, - stateProof : Proofs, + connProof : Proofs, clientProof : Proofs] \union [type : {"CHMsgConfirm"}, parameters : ConnectionParameters, - stateProof : Proofs] + connProof : Proofs] (* The set of all Init messages, such that the local end is the @@ -114,17 +124,6 @@ InitMsgs(le, re) == remoteEnd : re]] -(* TODO: find a more elegant way to do this. - What we want, ideally, is to specify this: - z \in x - to obtain a random Sequence z out of a set of sequences x, obtaining: - bufChainA \in InitMsgs - instead of the awkward lines for building bufChainA and bufChainB below. - *) -ChooseInitMsg(le, re) == - CHOOSE x \in InitMsgs(le, re) : TRUE - - (*************************************************************************** Environment actions. ***************************************************************************) @@ -135,7 +134,7 @@ ChooseInitMsg(le, re) == InitEnv == /\ maliciousEnv = TRUE /\ \/ /\ bufChainA \in {<> : msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} - /\ bufChainB = <<>> (* Empty buffer. *) + /\ bufChainB = <<>> (* Empty buffer initially. *) \/ /\ bufChainB \in {<> : msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} /\ bufChainA = <<>> @@ -155,7 +154,7 @@ MaliciousNextEnv == {Append(bufChainA, arbitraryMsg) : arbitraryMsg \in ConnectionHandshakeMessages} /\ UNCHANGED bufChainB - \/ /\ Len(bufChainA) < MaxBufLen - 1 + \/ /\ Len(bufChainB) < MaxBufLen - 1 /\ bufChainB' \in {Append(bufChainB, arbitraryMsg) : arbitraryMsg \in ConnectionHandshakeMessages} @@ -196,16 +195,27 @@ Next == \/ chmB!Next /\ UNCHANGED <> +FairProcessMsg == + \A m \in ConnectionHandshakeMessages : + /\ WF_chainAVars(chmA!ProcessMsg(m)) + /\ WF_chainBVars(chmB!ProcessMsg(m)) + +FairModuleProgress == + /\ WF_chainAVars(chmA!Next) + /\ WF_chainBVars(chmB!Next) + Spec == /\ Init /\ [][Next]_<> - /\ WF_chainAVars(chmA!Next) - /\ WF_chainBVars(chmB!Next) + /\ FairProcessMsg + /\ FairModuleProgress -TypeInvariant == - /\ bufChainA \in ConnectionHandshakeMessages - /\ bufChainB \in ConnectionHandshakeMessages +(* TODO: Unclear how to capture the type of a sequence. *) +\*TypeInvariant == +\* /\ \/ bufChainA = <<>> +\* \/ \A e in bufChainA : e \in ConnectionHandshakeMessages +\* /\ bufChainB \in ConnectionHandshakeMessages \* Liveness property. @@ -229,5 +239,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Thu May 07 13:36:50 CEST 2020 by adi +\* Last modified Tue May 12 13:36:09 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi + From 6fdf3517ba4aeff9167494559a1d6d5aa31def0d Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 12 May 2020 16:35:05 +0200 Subject: [PATCH 37/63] Possible solution found by removing the WF and adding explicit preconditions instead --- .../ConnectionHandshakeModule.tla | 110 +++++++++++------- .../spec/connection-handshake/Environment.tla | 36 +++--- 2 files changed, 87 insertions(+), 59 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index df3c06a7ab..39f2c22a30 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -19,6 +19,8 @@ VARIABLES store \* The local store of the chain running this chModule. +vars == <> + ConnectionEnds == [ connectionID : ConnectionIDs, @@ -88,6 +90,11 @@ ValidMessageType(type) == /\ type \in CHMessageTypes +ValidMsg(m) == + /\ ValidConnectionParameters(m.parameters) + /\ ValidMessageType(m.type) + + \* Given a ConnectionParameters record `para`, this operator returns a new set \* of parameters where the local and remote ends are flipped (i.e., reversed). FlipConnectionParameters(para) == @@ -116,9 +123,13 @@ GetClientProof == This action always enables (returns true); use with care. This action is analogous to "abortTransaction" function from ICS specs. *) -DropMsg(m) == - UNCHANGED<> +DropMsg(m) == + /\ UNCHANGED<> + +PreconditionsInitMsg(m) == + /\ store.connection.state = "UNINIT" + /\ Len(outBuf) < MaxBufLen (* Enables if we can reply on the buffer. *) (* Handles a "CHMsgInit" message 'm'. @@ -127,52 +138,66 @@ DropMsg(m) == buffer. *) HandleInitMsg(m) == - /\ store.connection.state = "UNINIT" - /\ Len(outBuf) < MaxBufLen \* Enabled if we can reply to the remote chain. - /\ LET newCon == [parameters |-> m.parameters, - state |-> "INIT"] - sProof == GetStateProof - cProof == GetClientProof - replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgTry", - stateProof |-> sProof, - clientProof |-> cProof] - IN /\ outBuf' = Append(outBuf, replyMsg) - /\ store' = [store EXCEPT !.connection = newCon] - - -(* Handles a "CHMsgTry" message. - *) -HandleTryMsg(m) == (* The good-case path. *) + \/ /\ PreconditionsInitMsg(m) = TRUE + /\ LET newCon == [parameters |-> m.parameters, + state |-> "INIT"] + sProof == GetStateProof + cProof == GetClientProof + replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgTry", + stateProof |-> sProof, + clientProof |-> cProof] + IN /\ outBuf' = Append(outBuf, replyMsg) + /\ store' = [store EXCEPT !.connection = newCon] + (* The exceptional path. *) + \/ /\ PreconditionsInitMsg(m) = FALSE + /\ DropMsg(m) + + +PreconditionsTryMsg(m) == /\ \/ store.connection.state = "UNINIT" \/ store.connection.state = "INIT" /\ CheckLocalParameters(m.parameters) /\ Len(outBuf) < MaxBufLen - /\ LET newCon == [parameters |-> m.parameters, - state |-> "INIT"] - sProof == GetStateProof - cProof == GetClientProof - replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgAck"] - IN /\ outBuf' = Append(outBuf, replyMsg) - /\ store' = [store EXCEPT !.connection = newCon] -(* Handles a "CHMsgAck" message. +(* Handles a "CHMsgTry" message. *) -HandleAckMsg(m) == +HandleTryMsg(m) == + \/ /\ PreconditionsTryMsg(m) = TRUE + /\ LET newCon == [parameters |-> m.parameters, + state |-> "INIT"] + sProof == GetStateProof + cProof == GetClientProof + replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgAck"] + IN /\ outBuf' = Append(outBuf, replyMsg) + /\ store' = [store EXCEPT !.connection = newCon] + \/ /\ PreconditionsTryMsg(m) = FALSE + /\ DropMsg(m) + + +PreconditionsAckMsg(m) == /\ \/ store.connection.state = "INIT" \/ store.connection.state = "TRYOPEN" /\ CheckLocalParameters(m.parameters) /\ Len(outBuf) < MaxBufLen - /\ LET newCon == [parameters |-> m.parameters, - state |-> "INIT"] - sProof == GetStateProof - cProof == GetClientProof - replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgAck"] - IN /\ outBuf' = Append(outBuf, replyMsg) - /\ store' = [store EXCEPT !.connection = newCon] + + +(* Handles a "CHMsgAck" message. + *) +HandleAckMsg(m) == + \/ /\ PreconditionsAckMsg(m) = TRUE + /\ LET newCon == [parameters |-> m.parameters, + state |-> "INIT"] + sProof == GetStateProof + cProof == GetClientProof + replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgAck"] + IN /\ outBuf' = Append(outBuf, replyMsg) + /\ store' = [store EXCEPT !.connection = newCon] + \/ /\ PreconditionsAckMsg(m) = FALSE + /\ DropMsg(m) (* Handles a "CHMsgConfirm" message. @@ -193,8 +218,7 @@ AdvanceChainHeight == Takes care of invoking priming the 'store' and any reply msg in 'outBuf'. *) ProcessMsg(m) == - /\ ValidConnectionParameters(m.parameters) = TRUE - /\ ValidMessageType(m.type) = TRUE + /\ ValidMsg(m) = TRUE (* One of the following disjunctions will always enable, since the message type is guaranteed to be valid in the THEN branch. *) /\ \/ m.type = "CHMsgInit" /\ HandleInitMsg(m) @@ -214,17 +238,17 @@ Init(chainID) == connection |-> nullConnection, client |-> nullClient] +\*FairProcessMsg == +\* \A m \in inBuf : /\ WF_vars(ProcessMsg(m)) Next == LET m == Head(inBuf) IN /\ inBuf # <<>> \* Enabled if we have an inbound msg. /\ inBuf' = Tail(inBuf) \* Strip the head of our inbound msg. buffer. - /\ \/ ProcessMsg(m) \* Generic action for handling a msg. - \/ DropMsg(m) - + /\ ProcessMsg(m) \* Generic action for handling a msg. ============================================================================= \* Modification History -\* Last modified Tue May 12 13:23:17 CEST 2020 by adi +\* Last modified Tue May 12 16:29:41 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index dc92183ed2..70ccab8181 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -32,14 +32,17 @@ chainBParameters == [ ClientIDs == { chainAParameters.clientID, chainBParameters.clientID } ConnectionIDs == { chainAParameters.connectionID, chainBParameters.connectionID } +chainAProcessVars == <> +chainBProcessVars == <> + (* Bundle with variables that chain A has access to. *) chainAVars == <> (* The local chain store. *) (* Bundle with variables that chain B has access to. *) -chainBVars == <> (* Local chain store. *) (* All variables specific to chains. *) @@ -133,9 +136,11 @@ InitMsgs(le, re) == or bufChainB. *) InitEnv == /\ maliciousEnv = TRUE - /\ \/ /\ bufChainA \in {<> : msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} + /\ \/ /\ bufChainA \in {<> : + msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} /\ bufChainB = <<>> (* Empty buffer initially. *) - \/ /\ bufChainB \in {<> : msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} + \/ /\ bufChainB \in {<> : + msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} /\ bufChainA = <<>> @@ -150,14 +155,12 @@ InitEnv == *) MaliciousNextEnv == \/ /\ Len(bufChainA) < MaxBufLen - 1 - /\ bufChainA' \in - {Append(bufChainA, arbitraryMsg) : - arbitraryMsg \in ConnectionHandshakeMessages} + /\ bufChainA' \in {Append(bufChainA, arbitraryMsg) : + arbitraryMsg \in ConnectionHandshakeMessages} /\ UNCHANGED bufChainB \/ /\ Len(bufChainB) < MaxBufLen - 1 - /\ bufChainB' \in - {Append(bufChainB, arbitraryMsg) : - arbitraryMsg \in ConnectionHandshakeMessages} + /\ bufChainB' \in {Append(bufChainB, arbitraryMsg) : + arbitraryMsg \in ConnectionHandshakeMessages} /\ UNCHANGED bufChainA @@ -195,10 +198,12 @@ Next == \/ chmB!Next /\ UNCHANGED <> -FairProcessMsg == - \A m \in ConnectionHandshakeMessages : - /\ WF_chainAVars(chmA!ProcessMsg(m)) - /\ WF_chainBVars(chmB!ProcessMsg(m)) +\*FairProcessMsg == +\* /\ \A m \in ConnectionHandshakeMessages : WF_chainAProcessVars(chmA!ProcessMsg(m)) +\* /\ \A n \in ConnectionHandshakeMessages : WF_chainBProcessVars(chmB!ProcessMsg(n)) +\* /\ chmA!FairProcessMsg +\* /\ chmB!FairProcessMsg + FairModuleProgress == /\ WF_chainAVars(chmA!Next) @@ -207,7 +212,6 @@ FairModuleProgress == Spec == /\ Init /\ [][Next]_<> - /\ FairProcessMsg /\ FairModuleProgress @@ -239,6 +243,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Tue May 12 13:36:09 CEST 2020 by adi +\* Last modified Tue May 12 16:33:51 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 41b0b4947c997b7f8195a1f02b5ae9a7b6fb1823 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 12 May 2020 17:23:18 +0200 Subject: [PATCH 38/63] HandleConfirmMsg, proofs mock, and ModifyStore general action. Model checking OK --- .../ConnectionHandshakeModule.tla | 80 ++++++++++++++----- .../spec/connection-handshake/Environment.tla | 11 +-- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 39f2c22a30..802042d189 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -106,12 +106,18 @@ FlipConnectionParameters(para) == connection in a certain state. *) GetStateProof == - [content |-> "state proof content"] + [content |-> "state proof content", + height |-> store.height, + connectionState |-> store.connection.state] (* TODO *) GetClientProof == - [content |-> "client proof content"] + IF store.client # nullClient + THEN [content |-> "client proof content", + height |-> store.client.height] + ELSE [content |-> "client proof content", + height |-> nullClient] (*************************************************************************** @@ -127,10 +133,26 @@ DropMsg(m) == /\ UNCHANGED<> + +(* Modifies the local store, by replacing the connection with the input 'newCon'. + If the height of the chains has not yet attained MaxHeight, this action also + advances the chain height. + *) +ModifyStore(newCon) == + \/ /\ store.height < MaxHeight + /\ store' = [store EXCEPT !.connection = newCon, + !.height = @ + 1] + \/ /\ store.height >= MaxHeight + /\ store' = [store EXCEPT !.connection = newCon] + + +(* State predicate, guarding the handler for the Init msg. + *) PreconditionsInitMsg(m) == /\ store.connection.state = "UNINIT" /\ Len(outBuf) < MaxBufLen (* Enables if we can reply on the buffer. *) + (* Handles a "CHMsgInit" message 'm'. Primes the store.connection to become initialized with the parameters @@ -149,12 +171,14 @@ HandleInitMsg(m) == stateProof |-> sProof, clientProof |-> cProof] IN /\ outBuf' = Append(outBuf, replyMsg) - /\ store' = [store EXCEPT !.connection = newCon] + /\ ModifyStore(newCon) (* The exceptional path. *) \/ /\ PreconditionsInitMsg(m) = FALSE /\ DropMsg(m) +(* State predicate, guarding the handler for the Try msg. + *) PreconditionsTryMsg(m) == /\ \/ store.connection.state = "UNINIT" \/ store.connection.state = "INIT" /\ CheckLocalParameters(m.parameters) @@ -172,11 +196,13 @@ HandleTryMsg(m) == replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "CHMsgAck"] IN /\ outBuf' = Append(outBuf, replyMsg) - /\ store' = [store EXCEPT !.connection = newCon] + /\ ModifyStore(newCon) \/ /\ PreconditionsTryMsg(m) = FALSE /\ DropMsg(m) +(* State predicate, guarding the handler for the Ack msg. + *) PreconditionsAckMsg(m) == /\ \/ store.connection.state = "INIT" \/ store.connection.state = "TRYOPEN" @@ -195,22 +221,36 @@ HandleAckMsg(m) == replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "CHMsgAck"] IN /\ outBuf' = Append(outBuf, replyMsg) - /\ store' = [store EXCEPT !.connection = newCon] + /\ ModifyStore(newCon) \/ /\ PreconditionsAckMsg(m) = FALSE /\ DropMsg(m) +(* State predicate, guarding the handler for the Confirm msg. + *) +PreconditionsConfirmMsg(m) == + /\ store.connection.state = "TRYOPEN" + /\ CheckLocalParameters(m.parameters) + /\ Len(outBuf) < MaxBufLen + + (* Handles a "CHMsgConfirm" message. *) HandleConfirmMsg(m) == - UNCHANGED<> + \/ /\ PreconditionsConfirmMsg(m) = TRUE + /\ LET newCon == [parameters |-> m.parameters, + state |-> "OPEN"] + IN /\ ModifyStore(newCon) + /\ UNCHANGED outBuf (* We're not sending any reply. *) + \/ /\ PreconditionsConfirmMsg(m) = FALSE + /\ DropMsg(m) \* If MaxHeight is not yet reached, then advance the height of the chain. AdvanceChainHeight == - /\ store.height < MaxHeight - /\ store' = [store EXCEPT !.height = @ + 1] - /\ UNCHANGED <> + \/ /\ store.height < MaxHeight + /\ store' = [store EXCEPT !.height = @ + 1] + \/ UNCHANGED <> (* Generic action for handling any type of inbound message. @@ -218,13 +258,12 @@ AdvanceChainHeight == Takes care of invoking priming the 'store' and any reply msg in 'outBuf'. *) ProcessMsg(m) == - /\ ValidMsg(m) = TRUE - (* One of the following disjunctions will always enable, since - the message type is guaranteed to be valid in the THEN branch. *) - /\ \/ m.type = "CHMsgInit" /\ HandleInitMsg(m) - \/ m.type = "CHMsgTry" /\ HandleTryMsg(m) - \/ m.type = "CHMsgAck" /\ HandleAckMsg(m) - \/ m.type = "CHMsgConfirm" /\ HandleConfirmMsg(m) + (* One of the following disjunctions will always enable, since + the message type is guaranteed to be valid in the THEN branch. *) + \/ m.type = "CHMsgInit" /\ HandleInitMsg(m) + \/ m.type = "CHMsgTry" /\ HandleTryMsg(m) + \/ m.type = "CHMsgAck" /\ HandleAckMsg(m) + \/ m.type = "CHMsgConfirm" /\ HandleConfirmMsg(m) (*************************************************************************** @@ -238,17 +277,18 @@ Init(chainID) == connection |-> nullConnection, client |-> nullClient] -\*FairProcessMsg == -\* \A m \in inBuf : /\ WF_vars(ProcessMsg(m)) Next == LET m == Head(inBuf) IN /\ inBuf # <<>> \* Enabled if we have an inbound msg. /\ inBuf' = Tail(inBuf) \* Strip the head of our inbound msg. buffer. - /\ ProcessMsg(m) \* Generic action for handling a msg. + /\ \/ /\ ValidMsg(m) + /\ ProcessMsg(m) \* Generic action for handling a msg. + \/ /\ ~ValidMsg(m) + /\ DropMsg(m) ============================================================================= \* Modification History -\* Last modified Tue May 12 16:29:41 CEST 2020 by adi +\* Last modified Tue May 12 17:17:29 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 70ccab8181..05c9e42303 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -32,8 +32,6 @@ chainBParameters == [ ClientIDs == { chainAParameters.clientID, chainBParameters.clientID } ConnectionIDs == { chainAParameters.connectionID, chainBParameters.connectionID } -chainAProcessVars == <> -chainBProcessVars == <> (* Bundle with variables that chain A has access to. *) chainAVars == <> -\*FairProcessMsg == -\* /\ \A m \in ConnectionHandshakeMessages : WF_chainAProcessVars(chmA!ProcessMsg(m)) -\* /\ \A n \in ConnectionHandshakeMessages : WF_chainBProcessVars(chmB!ProcessMsg(n)) -\* /\ chmA!FairProcessMsg -\* /\ chmB!FairProcessMsg - - FairModuleProgress == /\ WF_chainAVars(chmA!Next) /\ WF_chainBVars(chmB!Next) @@ -243,6 +234,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Tue May 12 16:33:51 CEST 2020 by adi +\* Last modified Tue May 12 16:38:51 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 34317f3f109646918f3e0169c8b7b791ada2dea3 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 13 May 2020 16:12:56 +0200 Subject: [PATCH 39/63] Stable version with sets for client.consensusStates --- .../ConnectionHandshakeModule.tla | 122 ++++++++++++------ .../spec/connection-handshake/Environment.tla | 65 +++++++--- 2 files changed, 131 insertions(+), 56 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 802042d189..9a9a1d6fb3 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -14,9 +14,12 @@ ASSUME Cardinality(ClientIDs) >= 1 VARIABLES + (******* + TODO store description block + *******) + store, \* The local store of the chain running this chModule. inBuf, \* A buffer (Sequence) holding any message(s) incoming to the chModule. - outBuf, \* A buffer (Sequence) holding outbound message(s) from the chModule. - store \* The local store of the chain running this chModule. + outBuf \* A buffer (Sequence) holding outbound message(s) from the chModule. vars == <> @@ -38,11 +41,17 @@ Heights == 1..MaxHeight Clients == [ clientID : ClientIDs, - consensusState : Heights + consensusState : { Heights } ] -nullClient == "no client" +(* TODO: + We should explain what is the semantic we are modelling. As this is simplified + view, we need to explain what property of proof (checking) we want to cover. +*) +\*Proofs == +\* [ +\* ] (* The set of all types which a message can take. @@ -95,8 +104,12 @@ ValidMsg(m) == /\ ValidMessageType(m.type) -\* Given a ConnectionParameters record `para`, this operator returns a new set -\* of parameters where the local and remote ends are flipped (i.e., reversed). +(* Operator for reversing the connection ends. + + Given a ConnectionParameters record 'para', returns a new set + of parameters where the local and remote ends are + flipped (i.e., reversed). + *) FlipConnectionParameters(para) == [localEnd |-> para.remoteEnd, remoteEnd |-> para.localEnd] @@ -105,27 +118,33 @@ FlipConnectionParameters(para) == (* Returns a proof, stating that the local store on this chain comprises a connection in a certain state. *) -GetStateProof == +GetStateProof(connState) == [content |-> "state proof content", - height |-> store.height, - connectionState |-> store.connection.state] + height |-> store.latestHeight, + connectionState |-> connState] (* TODO *) GetClientProof == - IF store.client # nullClient - THEN [content |-> "client proof content", - height |-> store.client.height] - ELSE [content |-> "client proof content", - height |-> nullClient] + [height |-> store.client.latestHeight] + +(* TODO: VERIFY PROOFS *) +VerifyStateProof(sp) == + TRUE + + +(* if \in set of consensusState from store.client. *) +VerifyClientProof(cp) == + TRUE (*************************************************************************** Connection Handshake Module actions & operators. ***************************************************************************) -(* Drops a message without any priming of local variables. +(* Drops a message without any priming of local variables. + This action always enables (returns true); use with care. This action is analogous to "abortTransaction" function from ICS specs. *) @@ -134,15 +153,17 @@ DropMsg(m) == -(* Modifies the local store, by replacing the connection with the input 'newCon'. - If the height of the chains has not yet attained MaxHeight, this action also +(* Modifies the local store. + + Replaces the connection in the store with the argument 'newCon'. + If the height (latestHeight) of the chain has not yet attained MaxHeight, this action also advances the chain height. *) ModifyStore(newCon) == - \/ /\ store.height < MaxHeight + \/ /\ store.latestHeight < MaxHeight /\ store' = [store EXCEPT !.connection = newCon, - !.height = @ + 1] - \/ /\ store.height >= MaxHeight + !.latestHeight = @ + 1] + \/ /\ store.latestHeight >= MaxHeight /\ store' = [store EXCEPT !.connection = newCon] @@ -150,6 +171,7 @@ ModifyStore(newCon) == *) PreconditionsInitMsg(m) == /\ store.connection.state = "UNINIT" +\* /\ currentHeight > rpoof.height /\ Len(outBuf) < MaxBufLen (* Enables if we can reply on the buffer. *) @@ -162,9 +184,9 @@ PreconditionsInitMsg(m) == HandleInitMsg(m) == (* The good-case path. *) \/ /\ PreconditionsInitMsg(m) = TRUE - /\ LET newCon == [parameters |-> m.parameters, + /\ LET newCon == [parameters |-> m.parameters, state |-> "INIT"] - sProof == GetStateProof + sProof == GetStateProof("INIT") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "CHMsgTry", @@ -181,7 +203,10 @@ HandleInitMsg(m) == *) PreconditionsTryMsg(m) == /\ \/ store.connection.state = "UNINIT" - \/ store.connection.state = "INIT" /\ CheckLocalParameters(m.parameters) + \/ /\ store.connection.state = "INIT" + /\ CheckLocalParameters(m.parameters) + /\ VerifyStateProof(m.stateProof) + /\ VerifyClientProof(m.clientProof) /\ Len(outBuf) < MaxBufLen @@ -190,8 +215,8 @@ PreconditionsTryMsg(m) == HandleTryMsg(m) == \/ /\ PreconditionsTryMsg(m) = TRUE /\ LET newCon == [parameters |-> m.parameters, - state |-> "INIT"] - sProof == GetStateProof + state |-> "TRYOPEN"] + sProof == GetStateProof("TRYOPEN") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "CHMsgAck"] @@ -207,6 +232,7 @@ PreconditionsAckMsg(m) == /\ \/ store.connection.state = "INIT" \/ store.connection.state = "TRYOPEN" /\ CheckLocalParameters(m.parameters) + /\ VerifyStateProof(m.stateProof) /\ Len(outBuf) < MaxBufLen @@ -215,8 +241,8 @@ PreconditionsAckMsg(m) == HandleAckMsg(m) == \/ /\ PreconditionsAckMsg(m) = TRUE /\ LET newCon == [parameters |-> m.parameters, - state |-> "INIT"] - sProof == GetStateProof + state |-> "OPEN"] + sProof == GetStateProof("OPEN") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "CHMsgAck"] @@ -231,6 +257,7 @@ HandleAckMsg(m) == PreconditionsConfirmMsg(m) == /\ store.connection.state = "TRYOPEN" /\ CheckLocalParameters(m.parameters) + /\ VerifyStateProof(m.stateProof) /\ Len(outBuf) < MaxBufLen @@ -246,20 +273,41 @@ HandleConfirmMsg(m) == /\ DropMsg(m) -\* If MaxHeight is not yet reached, then advance the height of the chain. +(* If MaxHeight is not yet reached, then advance the latestHeight of the chain. + *) AdvanceChainHeight == - \/ /\ store.height < MaxHeight - /\ store' = [store EXCEPT !.height = @ + 1] - \/ UNCHANGED <> + \/ /\ store.latestHeight < MaxHeight + /\ store' = [store EXCEPT !.latestHeight = @ + 1] + \/ /\ store.latestHeight >= MaxHeight + /\ UNCHANGED store + + +(* Action for updating the local client on this chain with a new height. + + This may prime the store, while leaving the buffers unchanged. + If the 'height' parameter already exists as part of + 'store.client.consensusStates', then we do not change the store. + This will also advance the chain height unless MaxHeight is reached. + *) +UpdateClient(height) == + \/ /\ (height \in store.client.consensusStates) + /\ UNCHANGED store + \/ /\ ~ (height \in store.client.consensusStates) + /\ \/ /\ store.latestHeight < MaxHeight + /\ store' = [store EXCEPT !.latestHeight = @ + 1, + !.client.consensusStates = @ \cup {height}] + \/ /\ store.latestHeight >= MaxHeight + /\ UNCHANGED store (* Generic action for handling any type of inbound message. + Expects as parameter a message. Takes care of invoking priming the 'store' and any reply msg in 'outBuf'. + This action assumes the message type is valid, therefore one of the + disjunctions will always enable. *) ProcessMsg(m) == - (* One of the following disjunctions will always enable, since - the message type is guaranteed to be valid in the THEN branch. *) \/ m.type = "CHMsgInit" /\ HandleInitMsg(m) \/ m.type = "CHMsgTry" /\ HandleTryMsg(m) \/ m.type = "CHMsgAck" /\ HandleAckMsg(m) @@ -271,11 +319,11 @@ ProcessMsg(m) == ***************************************************************************) -Init(chainID) == +Init(chainID, client) == /\ store = [id |-> chainID, - height |-> 1, + latestHeight |-> 1, connection |-> nullConnection, - client |-> nullClient] + client |-> client] Next == @@ -289,6 +337,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Tue May 12 17:17:29 CEST 2020 by adi +\* Last modified Wed May 13 16:09:56 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 05c9e42303..15faa995ba 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -1,5 +1,8 @@ ---------------------------- MODULE Environment ---------------------------- +(* TODO: clarify the name and purpose of this MODULE in the README.md. + This spec is wiring everything together. *) + EXTENDS Naturals, FiniteSets, Sequences @@ -19,18 +22,18 @@ VARIABLES chainAParameters == [ - connectionID |-> { "connAtoB" }, - clientID |-> { "clientChainA" } + connectionIDs |-> { "connAtoB" }, + clientIDs |-> { "clientIDChainA" } ] chainBParameters == [ - connectionID |-> { "connBtoA" }, - clientID |-> { "clientChainB" } + connectionIDs |-> { "connBtoA" }, + clientIDs |-> { "clientIDChainB" } ] -ClientIDs == { chainAParameters.clientID, chainBParameters.clientID } -ConnectionIDs == { chainAParameters.connectionID, chainBParameters.connectionID } +ClientIDs == { chainAParameters.clientIDs, chainBParameters.clientIDs } +ConnectionIDs == { chainAParameters.connectionIDs, chainBParameters.connectionIDs } (* Bundle with variables that chain A has access to. *) @@ -54,8 +57,8 @@ chmA == INSTANCE ConnectionHandshakeModule inBuf <- bufChainA, outBuf <- bufChainB, store <- storeChainA, - ConnectionIDs <- chainAParameters.connectionID, - ClientIDs <- chainAParameters.clientID + ConnectionIDs <- chainAParameters.connectionIDs, + ClientIDs <- chainAParameters.clientIDs chmB == INSTANCE ConnectionHandshakeModule @@ -63,13 +66,14 @@ chmB == INSTANCE ConnectionHandshakeModule inBuf <- bufChainB, \* Flip the message buffers w.r.t. chain A buffers. outBuf <- bufChainA, \* Inbound for "A" is outbound for "B". store <- storeChainB, - ConnectionIDs <- chainBParameters.connectionID, - ClientIDs <- chainBParameters.clientID + ConnectionIDs <- chainBParameters.connectionIDs, + ClientIDs <- chainBParameters.clientIDs ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} +(* TODO BLOCK COMMENTS *) ConnectionParameters == [ localEnd : chmA!ConnectionEnds, @@ -82,6 +86,8 @@ Connections == state : ConnectionStates ] +(* This is a mock proof. + TODO explain *) Proofs == [ height : 1..MaxHeight @@ -142,7 +148,16 @@ InitEnv == /\ bufChainA = <<>> -(* The environment overwrites the buffer of one of the chains. +(* May change either of the store of chain A or B. *) +GoodNextEnv == + \/ chmA!AdvanceChainHeight /\ UNCHANGED storeChainB + \/ chmB!AdvanceChainHeight /\ UNCHANGED storeChainA + \/ \E h \in { chmA!Heights } : + \/ chmA!UpdateClient(h) /\ UNCHANGED storeChainB + \/ chmB!UpdateClient(h) /\ UNCHANGED storeChainA + + +(* The environment injects a msg. in the buffer of one of the chains. This interferes with the CH protocol in two ways: 1. by introducing additional messages that are incorrect, 2. by dropping correct messages (overwritting them). @@ -163,12 +178,14 @@ MaliciousNextEnv == NextEnv == + \/ /\ GoodNextEnv + /\ UNCHANGED<> \/ /\ maliciousEnv = TRUE /\ MaliciousNextEnv - /\ UNCHANGED maliciousEnv + /\ UNCHANGED<> \/ /\ maliciousEnv = TRUE /\ maliciousEnv' = FALSE - /\ UNCHANGED<> + /\ UNCHANGED<> CHDone == @@ -182,16 +199,26 @@ CHDone == *****************************************************************************) +(* Initializes both chains, attributing to each a chainID as well as a client. + *) Init == - /\ chmA!Init("chainA") - /\ chmB!Init("chainB") + /\ \E cidA \in chainAParameters.clientIDs : + chmA!Init("chainA", + [clientID |-> cidA, + consensusStates |-> {}, (* Client state is an empty set. *) + latestHeight |-> 0]) + /\ \E cidB \in chainBParameters.clientIDs : + chmB!Init("chainB", + [clientID |-> cidB, + consensusStates |-> {}, + latestHeight |-> 0]) /\ InitEnv \* The two CH modules and the environment alternate their steps. Next == \/ CHDone - \/ NextEnv /\ UNCHANGED <> + \/ NextEnv \/ chmA!Next /\ UNCHANGED <> \/ chmB!Next /\ UNCHANGED <> @@ -216,8 +243,8 @@ Spec == \* Liveness property. Termination == <> ~ maliciousEnv - => <> /\ storeChainA.connection.state = "INIT" (* TODO: should be OPEN *) - /\ storeChainB.connection.state = "INIT" + => <> /\ storeChainA.connection.state = "OPEN" + /\ storeChainB.connection.state = "OPEN" \* Safety property. @@ -234,6 +261,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Tue May 12 16:38:51 CEST 2020 by adi +\* Last modified Wed May 13 16:11:41 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 199c31cc8db53420e3ab063a3401b4b2fdf8d046 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 13 May 2020 18:29:07 +0200 Subject: [PATCH 40/63] Fixed client initialization & termination bug (caused by full buffers) --- .../ConnectionHandshakeModule.tla | 37 +++++++------- .../spec/connection-handshake/Environment.tla | 48 ++++++++++--------- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 9a9a1d6fb3..ebedc0b087 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -28,20 +28,18 @@ ConnectionEnds == [ connectionID : ConnectionIDs, clientID : ClientIDs - \* commitmentPrefix add later ] nullConnection == [state |-> "UNINIT"] -Heights == 1..MaxHeight - - -Clients == +(* The set of possible initial values for the clients on this chain. *) +InitClients == [ - clientID : ClientIDs, - consensusState : { Heights } + consensusStates : {{}}, (* Client state is an empty set. *) + clientID : ClientIDs, (* Any ID is for the grabs. *) + latestHeight : {0} (* Initial height is 0. *) ] @@ -53,6 +51,7 @@ Clients == \* [ \* ] + (* The set of all types which a message can take. TODO: These types are also used in the Environment module. @@ -95,6 +94,7 @@ ValidConnectionParameters(para) == /\ CheckLocalParameters(para) +(* TODO: TypeOK. *) ValidMessageType(type) == /\ type \in CHMessageTypes @@ -119,8 +119,7 @@ FlipConnectionParameters(para) == connection in a certain state. *) GetStateProof(connState) == - [content |-> "state proof content", - height |-> store.latestHeight, + [height |-> store.latestHeight, connectionState |-> connState] @@ -131,6 +130,8 @@ GetClientProof == (* TODO: VERIFY PROOFS *) VerifyStateProof(sp) == +\* GetLatestHeight() < proof_height +\* /\ currentHeight > rpoof.height TRUE @@ -171,8 +172,6 @@ ModifyStore(newCon) == *) PreconditionsInitMsg(m) == /\ store.connection.state = "UNINIT" -\* /\ currentHeight > rpoof.height - /\ Len(outBuf) < MaxBufLen (* Enables if we can reply on the buffer. *) (* Handles a "CHMsgInit" message 'm'. @@ -184,6 +183,7 @@ PreconditionsInitMsg(m) == HandleInitMsg(m) == (* The good-case path. *) \/ /\ PreconditionsInitMsg(m) = TRUE + /\ Len(outBuf) < MaxBufLen (* Enables if we can reply on the buffer. *) /\ LET newCon == [parameters |-> m.parameters, state |-> "INIT"] sProof == GetStateProof("INIT") @@ -207,19 +207,21 @@ PreconditionsTryMsg(m) == /\ CheckLocalParameters(m.parameters) /\ VerifyStateProof(m.stateProof) /\ VerifyClientProof(m.clientProof) - /\ Len(outBuf) < MaxBufLen (* Handles a "CHMsgTry" message. *) HandleTryMsg(m) == \/ /\ PreconditionsTryMsg(m) = TRUE + /\ Len(outBuf) < MaxBufLen /\ LET newCon == [parameters |-> m.parameters, state |-> "TRYOPEN"] sProof == GetStateProof("TRYOPEN") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgAck"] + type |-> "CHMsgAck", + stateProof |-> sProof, + clientProof |-> cProof] IN /\ outBuf' = Append(outBuf, replyMsg) /\ ModifyStore(newCon) \/ /\ PreconditionsTryMsg(m) = FALSE @@ -233,19 +235,21 @@ PreconditionsAckMsg(m) == \/ store.connection.state = "TRYOPEN" /\ CheckLocalParameters(m.parameters) /\ VerifyStateProof(m.stateProof) - /\ Len(outBuf) < MaxBufLen (* Handles a "CHMsgAck" message. *) HandleAckMsg(m) == \/ /\ PreconditionsAckMsg(m) = TRUE + /\ Len(outBuf) < MaxBufLen /\ LET newCon == [parameters |-> m.parameters, state |-> "OPEN"] sProof == GetStateProof("OPEN") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgAck"] + type |-> "CHMsgConfirm", + stateProof |-> sProof, + clientProof |-> cProof] IN /\ outBuf' = Append(outBuf, replyMsg) /\ ModifyStore(newCon) \/ /\ PreconditionsAckMsg(m) = FALSE @@ -258,7 +262,6 @@ PreconditionsConfirmMsg(m) == /\ store.connection.state = "TRYOPEN" /\ CheckLocalParameters(m.parameters) /\ VerifyStateProof(m.stateProof) - /\ Len(outBuf) < MaxBufLen (* Handles a "CHMsgConfirm" message. @@ -337,6 +340,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Wed May 13 16:09:56 CEST 2020 by adi +\* Last modified Wed May 13 18:26:57 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 15faa995ba..01e131de0d 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -1,7 +1,13 @@ ---------------------------- MODULE Environment ---------------------------- -(* TODO: clarify the name and purpose of this MODULE in the README.md. - This spec is wiring everything together. *) +(* This module is part of the TLA+ specification for the + IBC Connection Handshake (CH) protocol. + + This module creates two instances of ConnectionHandshakeModule, + wires them together, simulates some malicious behavior, and also + provides an initialization so that the two instances can perform + the CH protocol. +*) EXTENDS Naturals, FiniteSets, Sequences @@ -63,8 +69,8 @@ chmA == INSTANCE ConnectionHandshakeModule chmB == INSTANCE ConnectionHandshakeModule WITH MaxHeight <- MaxHeight, - inBuf <- bufChainB, \* Flip the message buffers w.r.t. chain A buffers. - outBuf <- bufChainA, \* Inbound for "A" is outbound for "B". + inBuf <- bufChainB, (* Flip the message buffers w.r.t. chain A buffers. *) + outBuf <- bufChainA, (* Inbound for "A" is outbound for "B". *) store <- storeChainB, ConnectionIDs <- chainBParameters.connectionIDs, ClientIDs <- chainBParameters.clientIDs @@ -86,13 +92,20 @@ Connections == state : ConnectionStates ] -(* This is a mock proof. - TODO explain *) + +(* These are mock proofs. + + All proofs include a height; in addition, some proofs also contain other + fields (e.g., connectionState). *) Proofs == [ height : 1..MaxHeight ] + +Heights == 1..MaxHeight + + (******************************** Messages ******************************** These messages are connection handshake specific. @@ -152,9 +165,8 @@ InitEnv == GoodNextEnv == \/ chmA!AdvanceChainHeight /\ UNCHANGED storeChainB \/ chmB!AdvanceChainHeight /\ UNCHANGED storeChainA - \/ \E h \in { chmA!Heights } : - \/ chmA!UpdateClient(h) /\ UNCHANGED storeChainB - \/ chmB!UpdateClient(h) /\ UNCHANGED storeChainA + \/ \E hA \in Heights : chmA!UpdateClient(hA) /\ UNCHANGED storeChainB + \/ \E hB \in Heights : chmB!UpdateClient(hB) /\ UNCHANGED storeChainA (* The environment injects a msg. in the buffer of one of the chains. @@ -188,7 +200,7 @@ NextEnv == /\ UNCHANGED<> -CHDone == +CHProtocolDone == /\ storeChainA.connection.state = "INIT" (* TODO: should be OPEN *) /\ storeChainB.connection.state = "INIT" /\ UNCHANGED <> @@ -202,22 +214,14 @@ CHDone == (* Initializes both chains, attributing to each a chainID as well as a client. *) Init == - /\ \E cidA \in chainAParameters.clientIDs : - chmA!Init("chainA", - [clientID |-> cidA, - consensusStates |-> {}, (* Client state is an empty set. *) - latestHeight |-> 0]) - /\ \E cidB \in chainBParameters.clientIDs : - chmB!Init("chainB", - [clientID |-> cidB, - consensusStates |-> {}, - latestHeight |-> 0]) + /\ \E clientA \in chmA!InitClients : chmA!Init("chainA", clientA) + /\ \E clientB \in chmB!InitClients : chmB!Init("chainB", clientB) /\ InitEnv \* The two CH modules and the environment alternate their steps. Next == - \/ CHDone + \/ CHProtocolDone \/ NextEnv \/ chmA!Next /\ UNCHANGED <> \/ chmB!Next /\ UNCHANGED <> @@ -261,6 +265,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Wed May 13 16:11:41 CEST 2020 by adi +\* Last modified Wed May 13 18:15:52 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From dd426154b6e0f43e0a33f5c6d7c3a95ed7357406 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 13 May 2020 20:05:32 +0200 Subject: [PATCH 41/63] 3-step proof validation. Stuttering step is problematic --- .../ConnectionHandshakeModule.tla | 123 ++++++++++++------ .../spec/connection-handshake/Environment.tla | 16 ++- 2 files changed, 89 insertions(+), 50 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index ebedc0b087..55638551a7 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -6,8 +6,8 @@ EXTENDS Naturals, FiniteSets, Sequences CONSTANTS MaxHeight, \* Maximum height of the local chain. ConnectionIDs, \* The set of all possible connection IDs. ClientIDs, \* The set of all possible client IDs. - MaxBufLen \* Maximum length of the input and output buffers. - + MaxBufLen, \* Maximum length of the input and output buffers. + ConnectionStates \* All the possible connection states. ASSUME Cardinality(ConnectionIDs) >= 1 ASSUME Cardinality(ClientIDs) >= 1 @@ -37,9 +37,9 @@ nullConnection == [state |-> "UNINIT"] (* The set of possible initial values for the clients on this chain. *) InitClients == [ - consensusStates : {{}}, (* Client state is an empty set. *) + consensusStates : {{1}},(* Client initialized to height 1. *) clientID : ClientIDs, (* Any ID is for the grabs. *) - latestHeight : {0} (* Initial height is 0. *) + latestHeight : {1} (* The first height. *) ] @@ -47,9 +47,17 @@ InitClients == We should explain what is the semantic we are modelling. As this is simplified view, we need to explain what property of proof (checking) we want to cover. *) -\*Proofs == -\* [ -\* ] +ConnProofs == + [ + connectionState : ConnectionStates, + height : 1..MaxHeight + ] + + +ClientProofs == + [ + height : 1..MaxHeight + ] (* The set of all types which a message can take. @@ -95,13 +103,25 @@ ValidConnectionParameters(para) == (* TODO: TypeOK. *) -ValidMessageType(type) == - /\ type \in CHMessageTypes +ValidMessageType(m) == + m.type \in CHMessageTypes + + +ValidConnProof(m) == + m.connProof \in ConnProofs + + +ValidClientProof(m) == + m.clientProof \in ClientProofs ValidMsg(m) == /\ ValidConnectionParameters(m.parameters) - /\ ValidMessageType(m.type) + /\ ValidMessageType(m) + /\ \/ m.type = "CHMsgInit" + \/ m.type = "CHMsgTry" /\ ValidConnProof(m) /\ ValidClientProof(m) + \/ m.type = "CHMsgAck" /\ ValidConnProof(m) /\ ValidClientProof(m) + \/ m.type = "CHMsgConfirm" /\ ValidConnProof(m) (* Operator for reversing the connection ends. @@ -115,29 +135,37 @@ FlipConnectionParameters(para) == remoteEnd |-> para.localEnd] -(* Returns a proof, stating that the local store on this chain comprises a - connection in a certain state. +(* Returns a connection proof. + + The connection proof is used to demonstrate to another chain that the local store + on this chain comprises a connection in a certain state. *) -GetStateProof(connState) == +GetConnProof(connState) == [height |-> store.latestHeight, connectionState |-> connState] -(* TODO *) +(* Returns a client proof. + *) GetClientProof == [height |-> store.client.latestHeight] -(* TODO: VERIFY PROOFS *) -VerifyStateProof(sp) == -\* GetLatestHeight() < proof_height -\* /\ currentHeight > rpoof.height - TRUE +(* Verification of a connection proof. + + This is a state predicate returning true if the following holds: + 1. the local client stores the height reported in the proof, and + [eventually, this should become true] + *) +VerifyConnProof(cp) == + /\ cp.height \in store.client.consensusStates + +(* Verification of a client proof. -(* if \in set of consensusState from store.client. *) + if \in set of consensusState from store.client. *) VerifyClientProof(cp) == - TRUE + cp.height <= store.latestHeight (*************************************************************************** Connection Handshake Module actions & operators. @@ -153,7 +181,6 @@ DropMsg(m) == /\ UNCHANGED<> - (* Modifies the local store. Replaces the connection in the store with the argument 'newCon'. @@ -186,11 +213,11 @@ HandleInitMsg(m) == /\ Len(outBuf) < MaxBufLen (* Enables if we can reply on the buffer. *) /\ LET newCon == [parameters |-> m.parameters, state |-> "INIT"] - sProof == GetStateProof("INIT") + sProof == GetConnProof("INIT") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "CHMsgTry", - stateProof |-> sProof, + connProof |-> sProof, clientProof |-> cProof] IN /\ outBuf' = Append(outBuf, replyMsg) /\ ModifyStore(newCon) @@ -199,14 +226,16 @@ HandleInitMsg(m) == /\ DropMsg(m) -(* State predicate, guarding the handler for the Try msg. +(* State predicate, guarding the handler for the Try msg. + + If any of these preconditions does not hold, the message + is dropped. *) PreconditionsTryMsg(m) == /\ \/ store.connection.state = "UNINIT" \/ /\ store.connection.state = "INIT" /\ CheckLocalParameters(m.parameters) - /\ VerifyStateProof(m.stateProof) - /\ VerifyClientProof(m.clientProof) + /\ m.connProof.connectionState = "INIT" (* Handles a "CHMsgTry" message. @@ -214,13 +243,15 @@ PreconditionsTryMsg(m) == HandleTryMsg(m) == \/ /\ PreconditionsTryMsg(m) = TRUE /\ Len(outBuf) < MaxBufLen + /\ VerifyConnProof(m.connProof) + /\ VerifyClientProof(m.clientProof) /\ LET newCon == [parameters |-> m.parameters, state |-> "TRYOPEN"] - sProof == GetStateProof("TRYOPEN") + sProof == GetConnProof("TRYOPEN") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "CHMsgAck", - stateProof |-> sProof, + connProof |-> sProof, clientProof |-> cProof] IN /\ outBuf' = Append(outBuf, replyMsg) /\ ModifyStore(newCon) @@ -234,21 +265,21 @@ PreconditionsAckMsg(m) == /\ \/ store.connection.state = "INIT" \/ store.connection.state = "TRYOPEN" /\ CheckLocalParameters(m.parameters) - /\ VerifyStateProof(m.stateProof) - + /\ m.connProof.connectionState = "TRYOPEN" (* Handles a "CHMsgAck" message. *) HandleAckMsg(m) == \/ /\ PreconditionsAckMsg(m) = TRUE /\ Len(outBuf) < MaxBufLen + /\ VerifyConnProof(m.connProof) /\ LET newCon == [parameters |-> m.parameters, state |-> "OPEN"] - sProof == GetStateProof("OPEN") + sProof == GetConnProof("OPEN") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "CHMsgConfirm", - stateProof |-> sProof, + connProof |-> sProof, clientProof |-> cProof] IN /\ outBuf' = Append(outBuf, replyMsg) /\ ModifyStore(newCon) @@ -261,13 +292,14 @@ HandleAckMsg(m) == PreconditionsConfirmMsg(m) == /\ store.connection.state = "TRYOPEN" /\ CheckLocalParameters(m.parameters) - /\ VerifyStateProof(m.stateProof) + /\ m.connProof.connectionState = "OPEN" (* Handles a "CHMsgConfirm" message. *) HandleConfirmMsg(m) == \/ /\ PreconditionsConfirmMsg(m) = TRUE + /\ VerifyConnProof(m.connProof) /\ LET newCon == [parameters |-> m.parameters, state |-> "OPEN"] IN /\ ModifyStore(newCon) @@ -287,20 +319,25 @@ AdvanceChainHeight == (* Action for updating the local client on this chain with a new height. - This may prime the store, while leaving the buffers unchanged. - If the 'height' parameter already exists as part of - 'store.client.consensusStates', then we do not change the store. + This may prime the store; leaves the chain buffers unchanged. This will also advance the chain height unless MaxHeight is reached. + If the 'height' parameter already exists as part of + 'store.client.consensusStates', then we do not change the store. *) UpdateClient(height) == \/ /\ (height \in store.client.consensusStates) /\ UNCHANGED store \/ /\ ~ (height \in store.client.consensusStates) - /\ \/ /\ store.latestHeight < MaxHeight - /\ store' = [store EXCEPT !.latestHeight = @ + 1, - !.client.consensusStates = @ \cup {height}] - \/ /\ store.latestHeight >= MaxHeight - /\ UNCHANGED store + /\ LET newLHeight == IF height > store.client.latestHeight + THEN height + ELSE store.client.latestHeight + IN \/ /\ store.latestHeight < MaxHeight + /\ store' = [store EXCEPT !.latestHeight = @ + 1, + !.client.consensusStates = @ \cup {height}, + !.client.latestHeight = newLHeight] + \/ /\ store.latestHeight >= MaxHeight + /\ store' = [store EXCEPT !.client.consensusStates = @ \cup {height}, + !.client.latestHeight = newLHeight] (* Generic action for handling any type of inbound message. @@ -340,6 +377,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Wed May 13 18:26:57 CEST 2020 by adi +\* Last modified Wed May 13 20:02:12 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 01e131de0d..b43fabe3ab 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -57,6 +57,7 @@ chainStoreVars == <> allVars == <> +ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} chmA == INSTANCE ConnectionHandshakeModule WITH MaxHeight <- MaxHeight, @@ -76,9 +77,6 @@ chmB == INSTANCE ConnectionHandshakeModule ClientIDs <- chainBParameters.clientIDs -ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} - - (* TODO BLOCK COMMENTS *) ConnectionParameters == [ @@ -161,12 +159,16 @@ InitEnv == /\ bufChainA = <<>> -(* May change either of the store of chain A or B. *) +(* May change either of the store of chain A or B. + + TODO: Unclear what condition we want precisely! *) GoodNextEnv == \/ chmA!AdvanceChainHeight /\ UNCHANGED storeChainB \/ chmB!AdvanceChainHeight /\ UNCHANGED storeChainA - \/ \E hA \in Heights : chmA!UpdateClient(hA) /\ UNCHANGED storeChainB - \/ \E hB \in Heights : chmB!UpdateClient(hB) /\ UNCHANGED storeChainA +\* \/ \E hA \in Heights : chmA!UpdateClient(hA) /\ UNCHANGED storeChainB +\* \/ \E hB \in Heights : chmB!UpdateClient(hB) /\ UNCHANGED storeChainA + \/ chmA!UpdateClient(storeChainB.latestHeight) /\ UNCHANGED storeChainB + \/ chmB!UpdateClient(storeChainA.latestHeight) /\ UNCHANGED storeChainA (* The environment injects a msg. in the buffer of one of the chains. @@ -265,6 +267,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Wed May 13 18:15:52 CEST 2020 by adi +\* Last modified Wed May 13 19:59:49 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 7313e004f7466aa2eefe72f58ffd35a64c2dab02 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 14 May 2020 10:04:13 +0200 Subject: [PATCH 42/63] Fixed stuttering problem w/ a fairness constraint. --- verification/spec/connection-handshake/Environment.tla | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index b43fabe3ab..6599e17c21 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -229,14 +229,16 @@ Next == \/ chmB!Next /\ UNCHANGED <> -FairModuleProgress == +FairProgress == /\ WF_chainAVars(chmA!Next) /\ WF_chainBVars(chmB!Next) + /\ WF_chainStoreVars(GoodNextEnv) + Spec == /\ Init /\ [][Next]_<> - /\ FairModuleProgress + /\ FairProgress (* TODO: Unclear how to capture the type of a sequence. *) @@ -267,6 +269,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Wed May 13 19:59:49 CEST 2020 by adi +\* Last modified Thu May 14 10:01:23 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 072a23fa17514702f0e79573a54ac86f56965812 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 14 May 2020 10:59:34 +0200 Subject: [PATCH 43/63] Cleaned-up & added comments to CHM --- .../ConnectionHandshakeModule.tla | 188 +++++++++++++++--- 1 file changed, 155 insertions(+), 33 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 55638551a7..742cfeb9a7 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -1,5 +1,29 @@ --------------------- MODULE ConnectionHandshakeModule --------------------- +(*************************************************************************** + + This module is part of the TLA+ specification for the + IBC Connection Handshake (CH) protocol. + + This module captures the actions and operators of the IBC CH protocol. + Typically, it is an IBC module running on a chain that would implement + this logic, hence the name "ConnectionHandshakeModule". + + This module deals with a high-level spec of the CH protocol, hence it is + a simplification in several regards: + - the modules assumes to run on a chain which modeled as a simple + advancing height, plus a few more critical fields (see the 'store'), + but without any state (e.g., blockchain, transactions, consensus core); + - we model a single connection; establishing multiple connections is not + possible; + - we do not do any cryptographic proofs or proof verifications. + - the abstractions we use are higher-level, and slightly different from + the ones in ICS 003 (see e.g., ConnectionEnd and Connection) + - the client colocated with the module is simplified, comprising only + a set of heights. + + ***************************************************************************) + EXTENDS Naturals, FiniteSets, Sequences @@ -14,16 +38,45 @@ ASSUME Cardinality(ClientIDs) >= 1 VARIABLES - (******* - TODO store description block - *******) - store, \* The local store of the chain running this chModule. - inBuf, \* A buffer (Sequence) holding any message(s) incoming to the chModule. - outBuf \* A buffer (Sequence) holding outbound message(s) from the chModule. +(******************************* Store ***************************** + The store record of a chain contains the following fields: + + - id -- a string + Stores the identifier of the chain where this module executes. + + - latestHeight -- a Nat + Describes the current height of the chain. + + - connection -- a connection record. + Captures all the details of the connection on this chain. + For a full description of a connection record, see the + 'Environment.Connections' set. + + - client -- a client record. + Specifies the state of the client running on this chain. + For a full description of a client and the initialization values + for this record, see the InitClients set below. + + ***************************************************************************) + store, + inBuf, \* A buffer (Sequence) holding any message(s) incoming to the chModule. + outBuf \* A buffer (Sequence) holding outbound message(s) from the chModule. vars == <> + +(******************************* ConnectionEnds ***************************** + A set of connection end records. + A connection end record contains the following fields: + + - connectionID -- a string + Stores the identifier of this connection, specific to a chain. + + - clientID -- a string + Stores the identifier of the client running on this chain. + + ***************************************************************************) ConnectionEnds == [ connectionID : ConnectionIDs, @@ -31,22 +84,55 @@ ConnectionEnds == ] +(* + Initially, the connection on this chain is uninitialized. +*) nullConnection == [state |-> "UNINIT"] -(* The set of possible initial values for the clients on this chain. *) +(******************************* InitClients ***************************** + A set of records describing the possible initial values for the + clients on this chain. + + A client record contains the following fields: + + - consensusStates -- a set of heights, each height being a Nat + Stores the set of all heights (i.e., consensus states) that this + client observed. At initialization time, the client only observes + the first height, so the only possible value for this record is + {1}. + + - clientID -- a string + The identifier of the client. + + - latestHeight -- a natural number + Stores the latest height among all the heights in consensusStates. + Initialized to 1. + + ***************************************************************************) InitClients == [ - consensusStates : {{1}},(* Client initialized to height 1. *) - clientID : ClientIDs, (* Any ID is for the grabs. *) - latestHeight : {1} (* The first height. *) + consensusStates : {{1}}, + clientID : ClientIDs, + latestHeight : {1} ] -(* TODO: - We should explain what is the semantic we are modelling. As this is simplified - view, we need to explain what property of proof (checking) we want to cover. -*) +(******************************* ConnProof ********************************* + + A set of records describing the possible values for connection proofs. + + A connection proof record contains the following fields: + + - connectionState -- a string + Captures the state of the connection in the local store of the module + which created this proof. + + - height -- a Nat + The current height (latestHeight) of the chain at the moment when the + module created this proof. + + ***************************************************************************) ConnProofs == [ connectionState : ConnectionStates, @@ -54,25 +140,44 @@ ConnProofs == ] +(******************************* ClientProofs ******************************* + + A set of records describing the possible values for client proofs. + + A client proof record contains the following fields: + + - height -- a Nat + The current height (latestHeight) of the client colocated with module + which created this proof. + + ***************************************************************************) ClientProofs == [ height : 1..MaxHeight ] -(* The set of all types which a message can take. - - TODO: These types are also used in the Environment module. - Is it possible to restrict these to a single module? - *) +(******************************* CHMessageTypes ***************************** + + The set of valid message types that this module can handle, e.g., as + incoming or outgoing messages. + + For a complete description of the message record, see + 'Environment.Messages'. + + ***************************************************************************) CHMessageTypes == - { "CHMsgInit", "CHMsgTry", "CHMsgAck", "CHMsgConfirm"} + {"CHMsgInit", + "CHMsgTry", + "CHMsgAck", + "CHMsgConfirm"} (*************************************************************************** - Helper operators. + Helper operators. ***************************************************************************) + (* Returns true if 'para' matches the parameters in the local connection, and returns false otherwise. *) @@ -102,11 +207,21 @@ ValidConnectionParameters(para) == /\ CheckLocalParameters(para) -(* TODO: TypeOK. *) +(* Basic validation on the type of an incoming message. + *) ValidMessageType(m) == m.type \in CHMessageTypes +(* Basic validation on an incoming message, including the + connection proof and client proofs. + + The following three formulas are state predicates, + not type invariants! Some proofs are generated by the + malicious environment, and therefore may be malformed. + Our specification must be able to capture such malformed + records, and these state predicates helps filter them out. + *) ValidConnProof(m) == m.connProof \in ConnProofs @@ -135,17 +250,17 @@ FlipConnectionParameters(para) == remoteEnd |-> para.localEnd] -(* Returns a connection proof. +(* Operator for construcing a connection proof. - The connection proof is used to demonstrate to another chain that the local store - on this chain comprises a connection in a certain state. + The connection proof is used to demonstrate to another chain that the + local store on this chain comprises a connection in a certain state. *) GetConnProof(connState) == [height |-> store.latestHeight, connectionState |-> connState] -(* Returns a client proof. +(* Operator for construcing a client proof. *) GetClientProof == [height |-> store.client.latestHeight] @@ -153,9 +268,11 @@ GetClientProof == (* Verification of a connection proof. - This is a state predicate returning true if the following holds: - 1. the local client stores the height reported in the proof, and - [eventually, this should become true] + This is a state predicate returning true if the following holds: the local + client on this chain stores the height reported in the proof. Note that this + condition should eventually become true, assuming a correct environment, which + should periodically update the client on each chain; see actions 'GoodNextEnv' + and 'UpdateClient'. *) VerifyConnProof(cp) == /\ cp.height \in store.client.consensusStates @@ -163,16 +280,20 @@ VerifyConnProof(cp) == (* Verification of a client proof. - if \in set of consensusState from store.client. *) + This is a state predicate returning true if the following holds: the height + reported in the client proof must not exceed the current (latestHeight) of + this chain. + *) VerifyClientProof(cp) == cp.height <= store.latestHeight + (*************************************************************************** Connection Handshake Module actions & operators. ***************************************************************************) -(* Drops a message without any priming of local variables. +(* Drops a message without priming the store or output buffer variables. This action always enables (returns true); use with care. This action is analogous to "abortTransaction" function from ICS specs. @@ -233,7 +354,7 @@ HandleInitMsg(m) == *) PreconditionsTryMsg(m) == /\ \/ store.connection.state = "UNINIT" - \/ /\ store.connection.state = "INIT" + \/ /\ store.connection.state = "INIT" /\ CheckLocalParameters(m.parameters) /\ m.connProof.connectionState = "INIT" @@ -267,6 +388,7 @@ PreconditionsAckMsg(m) == /\ CheckLocalParameters(m.parameters) /\ m.connProof.connectionState = "TRYOPEN" + (* Handles a "CHMsgAck" message. *) HandleAckMsg(m) == @@ -377,6 +499,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Wed May 13 20:02:12 CEST 2020 by adi +\* Last modified Thu May 14 10:58:32 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi From 1044fb24bd023593adf066f5e9a74529aae7719f Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 14 May 2020 16:50:35 +0200 Subject: [PATCH 44/63] Handlers overhaul after input from Anca & Zarko --- .../ConnectionHandshakeModule.tla | 261 ++++++------------ .../spec/connection-handshake/Environment.tla | 126 ++++++--- .../spec/connection-handshake/README.md | 28 ++ 3 files changed, 200 insertions(+), 215 deletions(-) create mode 100644 verification/spec/connection-handshake/README.md diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 742cfeb9a7..ceaa1de2a9 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -63,7 +63,7 @@ VARIABLES outBuf \* A buffer (Sequence) holding outbound message(s) from the chModule. -vars == <> +moduleVars == <> (******************************* ConnectionEnds ***************************** @@ -118,45 +118,6 @@ InitClients == ] -(******************************* ConnProof ********************************* - - A set of records describing the possible values for connection proofs. - - A connection proof record contains the following fields: - - - connectionState -- a string - Captures the state of the connection in the local store of the module - which created this proof. - - - height -- a Nat - The current height (latestHeight) of the chain at the moment when the - module created this proof. - - ***************************************************************************) -ConnProofs == - [ - connectionState : ConnectionStates, - height : 1..MaxHeight - ] - - -(******************************* ClientProofs ******************************* - - A set of records describing the possible values for client proofs. - - A client proof record contains the following fields: - - - height -- a Nat - The current height (latestHeight) of the client colocated with module - which created this proof. - - ***************************************************************************) -ClientProofs == - [ - height : 1..MaxHeight - ] - - (******************************* CHMessageTypes ***************************** The set of valid message types that this module can handle, e.g., as @@ -207,38 +168,6 @@ ValidConnectionParameters(para) == /\ CheckLocalParameters(para) -(* Basic validation on the type of an incoming message. - *) -ValidMessageType(m) == - m.type \in CHMessageTypes - - -(* Basic validation on an incoming message, including the - connection proof and client proofs. - - The following three formulas are state predicates, - not type invariants! Some proofs are generated by the - malicious environment, and therefore may be malformed. - Our specification must be able to capture such malformed - records, and these state predicates helps filter them out. - *) -ValidConnProof(m) == - m.connProof \in ConnProofs - - -ValidClientProof(m) == - m.clientProof \in ClientProofs - - -ValidMsg(m) == - /\ ValidConnectionParameters(m.parameters) - /\ ValidMessageType(m) - /\ \/ m.type = "CHMsgInit" - \/ m.type = "CHMsgTry" /\ ValidConnProof(m) /\ ValidClientProof(m) - \/ m.type = "CHMsgAck" /\ ValidConnProof(m) /\ ValidClientProof(m) - \/ m.type = "CHMsgConfirm" /\ ValidConnProof(m) - - (* Operator for reversing the connection ends. Given a ConnectionParameters record 'para', returns a new set @@ -275,7 +204,7 @@ GetClientProof == and 'UpdateClient'. *) VerifyConnProof(cp) == - /\ cp.height \in store.client.consensusStates + /\ cp.height \in store.client.consensusStates (* Verification of a client proof. @@ -293,33 +222,15 @@ VerifyClientProof(cp) == ***************************************************************************) -(* Drops a message without priming the store or output buffer variables. - - This action always enables (returns true); use with care. - This action is analogous to "abortTransaction" function from ICS specs. - *) -DropMsg(m) == - /\ UNCHANGED<> - - (* Modifies the local store. Replaces the connection in the store with the argument 'newCon'. If the height (latestHeight) of the chain has not yet attained MaxHeight, this action also advances the chain height. *) -ModifyStore(newCon) == - \/ /\ store.latestHeight < MaxHeight - /\ store' = [store EXCEPT !.connection = newCon, - !.latestHeight = @ + 1] - \/ /\ store.latestHeight >= MaxHeight - /\ store' = [store EXCEPT !.connection = newCon] - - -(* State predicate, guarding the handler for the Init msg. - *) -PreconditionsInitMsg(m) == - /\ store.connection.state = "UNINIT" +NewStore(newCon) == + [store EXCEPT !.connection = newCon, + !.latestHeight = @ + 1] (* Handles a "CHMsgInit" message 'm'. @@ -329,22 +240,20 @@ PreconditionsInitMsg(m) == buffer. *) HandleInitMsg(m) == - (* The good-case path. *) - \/ /\ PreconditionsInitMsg(m) = TRUE - /\ Len(outBuf) < MaxBufLen (* Enables if we can reply on the buffer. *) - /\ LET newCon == [parameters |-> m.parameters, - state |-> "INIT"] - sProof == GetConnProof("INIT") - cProof == GetClientProof - replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgTry", - connProof |-> sProof, - clientProof |-> cProof] - IN /\ outBuf' = Append(outBuf, replyMsg) - /\ ModifyStore(newCon) - (* The exceptional path. *) - \/ /\ PreconditionsInitMsg(m) = FALSE - /\ DropMsg(m) + LET newCon == [parameters |-> m.parameters, + state |-> "INIT"] + sProof == GetConnProof("INIT") + cProof == GetClientProof + replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgTry", + connProof |-> sProof, + clientProof |-> cProof] IN + IF /\ ValidConnectionParameters(m.parameters) + /\ store.connection.state = "UNINIT" + THEN [out |-> Append(outBuf, replyMsg), + store |-> NewStore(newCon)] + ELSE [out |-> outBuf, + store |-> store] (* State predicate, guarding the handler for the Try msg. @@ -353,31 +262,31 @@ HandleInitMsg(m) == is dropped. *) PreconditionsTryMsg(m) == + /\ ValidConnectionParameters(m.parameters) /\ \/ store.connection.state = "UNINIT" \/ /\ store.connection.state = "INIT" /\ CheckLocalParameters(m.parameters) /\ m.connProof.connectionState = "INIT" + /\ VerifyConnProof(m.connProof) + /\ VerifyClientProof(m.clientProof) (* Handles a "CHMsgTry" message. *) HandleTryMsg(m) == - \/ /\ PreconditionsTryMsg(m) = TRUE - /\ Len(outBuf) < MaxBufLen - /\ VerifyConnProof(m.connProof) - /\ VerifyClientProof(m.clientProof) - /\ LET newCon == [parameters |-> m.parameters, - state |-> "TRYOPEN"] - sProof == GetConnProof("TRYOPEN") - cProof == GetClientProof - replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgAck", - connProof |-> sProof, - clientProof |-> cProof] - IN /\ outBuf' = Append(outBuf, replyMsg) - /\ ModifyStore(newCon) - \/ /\ PreconditionsTryMsg(m) = FALSE - /\ DropMsg(m) + LET newCon == [parameters |-> m.parameters, + state |-> "TRYOPEN"] + sProof == GetConnProof("TRYOPEN") + cProof == GetClientProof + replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgAck", + connProof |-> sProof, + clientProof |-> cProof] IN + IF PreconditionsTryMsg(m) + THEN [out |-> Append(outBuf, replyMsg), + store |-> NewStore(newCon)] + ELSE [out |-> outBuf, + store |-> store] (* State predicate, guarding the handler for the Ack msg. @@ -387,26 +296,25 @@ PreconditionsAckMsg(m) == \/ store.connection.state = "TRYOPEN" /\ CheckLocalParameters(m.parameters) /\ m.connProof.connectionState = "TRYOPEN" + /\ VerifyConnProof(m.connProof) (* Handles a "CHMsgAck" message. *) HandleAckMsg(m) == - \/ /\ PreconditionsAckMsg(m) = TRUE - /\ Len(outBuf) < MaxBufLen - /\ VerifyConnProof(m.connProof) - /\ LET newCon == [parameters |-> m.parameters, - state |-> "OPEN"] - sProof == GetConnProof("OPEN") - cProof == GetClientProof - replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgConfirm", - connProof |-> sProof, - clientProof |-> cProof] - IN /\ outBuf' = Append(outBuf, replyMsg) - /\ ModifyStore(newCon) - \/ /\ PreconditionsAckMsg(m) = FALSE - /\ DropMsg(m) + LET newCon == [parameters |-> m.parameters, + state |-> "OPEN"] + sProof == GetConnProof("OPEN") + cProof == GetClientProof + replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), + type |-> "CHMsgConfirm", + connProof |-> sProof, + clientProof |-> cProof] IN + IF PreconditionsAckMsg(m) + THEN [out |-> Append(outBuf, replyMsg), + store |-> NewStore(newCon)] + ELSE [out |-> outBuf, + store |-> store] (* State predicate, guarding the handler for the Confirm msg. @@ -415,28 +323,28 @@ PreconditionsConfirmMsg(m) == /\ store.connection.state = "TRYOPEN" /\ CheckLocalParameters(m.parameters) /\ m.connProof.connectionState = "OPEN" - + /\ VerifyConnProof(m.connProof) (* Handles a "CHMsgConfirm" message. *) HandleConfirmMsg(m) == - \/ /\ PreconditionsConfirmMsg(m) = TRUE - /\ VerifyConnProof(m.connProof) - /\ LET newCon == [parameters |-> m.parameters, - state |-> "OPEN"] - IN /\ ModifyStore(newCon) - /\ UNCHANGED outBuf (* We're not sending any reply. *) - \/ /\ PreconditionsConfirmMsg(m) = FALSE - /\ DropMsg(m) + LET newCon == [parameters |-> m.parameters, + state |-> "OPEN"] IN + IF PreconditionsConfirmMsg(m) + THEN [out |-> outBuf, (* Never need to reply to a confirm msg. *) + store |-> NewStore(newCon)] + ELSE [out |-> outBuf, + store |-> store] (* If MaxHeight is not yet reached, then advance the latestHeight of the chain. *) AdvanceChainHeight == - \/ /\ store.latestHeight < MaxHeight - /\ store' = [store EXCEPT !.latestHeight = @ + 1] - \/ /\ store.latestHeight >= MaxHeight - /\ UNCHANGED store + /\ store' = [store EXCEPT !.latestHeight = @ + 1] + + +NotMaxHeight == + store.latestHeight <= MaxHeight (* Action for updating the local client on this chain with a new height. @@ -447,19 +355,9 @@ AdvanceChainHeight == 'store.client.consensusStates', then we do not change the store. *) UpdateClient(height) == - \/ /\ (height \in store.client.consensusStates) - /\ UNCHANGED store - \/ /\ ~ (height \in store.client.consensusStates) - /\ LET newLHeight == IF height > store.client.latestHeight - THEN height - ELSE store.client.latestHeight - IN \/ /\ store.latestHeight < MaxHeight - /\ store' = [store EXCEPT !.latestHeight = @ + 1, - !.client.consensusStates = @ \cup {height}, - !.client.latestHeight = newLHeight] - \/ /\ store.latestHeight >= MaxHeight - /\ store' = [store EXCEPT !.client.consensusStates = @ \cup {height}, - !.client.latestHeight = newLHeight] + store' = [store EXCEPT !.latestHeight = @ + 1, + !.client.consensusStates = @ \cup {height}, + !.client.latestHeight = height] (* Generic action for handling any type of inbound message. @@ -470,10 +368,12 @@ UpdateClient(height) == disjunctions will always enable. *) ProcessMsg(m) == - \/ m.type = "CHMsgInit" /\ HandleInitMsg(m) - \/ m.type = "CHMsgTry" /\ HandleTryMsg(m) - \/ m.type = "CHMsgAck" /\ HandleAckMsg(m) - \/ m.type = "CHMsgConfirm" /\ HandleConfirmMsg(m) + LET res == CASE m.type = "CHMsgInit" -> HandleInitMsg(m) + [] m.type = "CHMsgTry" -> HandleTryMsg(m) + [] m.type = "CHMsgAck" -> HandleAckMsg(m) + [] m.type = "CHMsgConfirm" -> HandleConfirmMsg(m) IN + /\ outBuf' = res.out + /\ store' = res.store (*************************************************************************** @@ -489,16 +389,19 @@ Init(chainID, client) == Next == - LET m == Head(inBuf) - IN /\ inBuf # <<>> \* Enabled if we have an inbound msg. - /\ inBuf' = Tail(inBuf) \* Strip the head of our inbound msg. buffer. - /\ \/ /\ ValidMsg(m) - /\ ProcessMsg(m) \* Generic action for handling a msg. - \/ /\ ~ValidMsg(m) - /\ DropMsg(m) + \/ /\ inBuf # <<>> \* Enabled if we have an inbound msg. + /\ ProcessMsg(Head(inBuf)) \* Generic action for handling a msg. + /\ inBuf' = Tail(inBuf) \* Strip the head of our inbound msg. buffer. + \/ /\ inBuf = <<>> + /\ UNCHANGED<> + + +\* /\ store.latestHeight < MaxHeight +\* /\ store.latestHeight = MaxHeight + ============================================================================= \* Modification History -\* Last modified Thu May 14 10:58:32 CEST 2020 by adi +\* Last modified Thu May 14 16:30:38 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 6599e17c21..08bcc13bc1 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -1,13 +1,22 @@ ---------------------------- MODULE Environment ---------------------------- -(* This module is part of the TLA+ specification for the - IBC Connection Handshake (CH) protocol. +(*************************************************************************** + + This module is part of the TLA+ specification for the + IBC Connection Handshake (CH) protocol. This is a high-level spec of CH. - This module creates two instances of ConnectionHandshakeModule, - wires them together, simulates some malicious behavior, and also - provides an initialization so that the two instances can perform - the CH protocol. -*) + This module captures the operators and actions outside of the CH protocol + itself (i.e., the environment). + Among others, the environment does the following: + - creates two instances of ConnectionHandshakeModule, + - wires these instances together, + - simulates some malicious behavior by injecting incorrect messages + - provides the initialization step for CH protocol, concretely a + "CHMsgInit" message, so that the two instances can perform the protocol. + - updates the clients on each instance periodically, or advances the chain + of each instance. + + ***************************************************************************) EXTENDS Naturals, FiniteSets, Sequences @@ -84,6 +93,18 @@ ConnectionParameters == remoteEnd : chmB!ConnectionEnds ] + +(******************************* Connections ******************************* + A set of connection end records. + A connection end record contains the following fields: + + - connectionID -- a string + Stores the identifier of this connection, specific to a chain. + + - clientID -- a string + Stores the identifier of the client running on this chain. + + ***************************************************************************) Connections == [ parameters : ConnectionParameters, @@ -91,11 +112,38 @@ Connections == ] -(* These are mock proofs. +(******************************* ConnProof ********************************* + A set of records describing the possible values for connection proofs. + + A connection proof record contains the following fields: - All proofs include a height; in addition, some proofs also contain other - fields (e.g., connectionState). *) -Proofs == + - connectionState -- a string + Captures the state of the connection in the local store of the module + which created this proof. + + - height -- a Nat + The current height (latestHeight) of the chain at the moment when the + module created this proof. + + ***************************************************************************) +ConnProofs == + [ + connectionState : ConnectionStates, + height : 1..MaxHeight + ] + + +(******************************* ClientProofs ******************************* + A set of records describing the possible values for client proofs. + + A client proof record contains the following fields: + + - height -- a Nat + The current height (latestHeight) of the client colocated with module + which created this proof. + + ***************************************************************************) +ClientProofs == [ height : 1..MaxHeight ] @@ -105,15 +153,20 @@ Heights == 1..MaxHeight (******************************** Messages ******************************** - These messages are connection handshake specific. +Messages are connection handshake specific. - In the low-level connection handshake protocol, the four messages have the - following types: ConnOpenInit, ConnOpenTry, ConnOpenAck, ConnOpenConfirm. + The valid message types are defined in: + ConnectionHandshakeModule.CHMessageTypes. + +In the low-level connection handshake protocol, the four messages have the +following types: ConnOpenInit, ConnOpenTry, ConnOpenAck, ConnOpenConfirm. These are described in ICS 003. In this high-level specification, we choose slightly different names, to make an explicit distinction to the low-level protocol. Message types are as follows: CHMsgInit, CHMsgTry, CHMsgAck, and CHMsgConfirm. Notice that the fields of each message are also different to the ICS 003 specification. + + ***************************************************************************) ConnectionHandshakeMessages == [type : {"CHMsgInit"}, @@ -121,17 +174,17 @@ ConnectionHandshakeMessages == \union [type : {"CHMsgTry"}, parameters : ConnectionParameters, - connProof : Proofs, - clientProof : Proofs] + connProof : ConnProofs, + clientProof : ClientProofs] \union [type : {"CHMsgAck"}, parameters : ConnectionParameters, - connProof : Proofs, - clientProof : Proofs] + connProof : ConnProofs, + clientProof : ClientProofs] \union [type : {"CHMsgConfirm"}, parameters : ConnectionParameters, - connProof : Proofs] + connProof : ConnProofs] (* The set of all Init messages, such that the local end is the @@ -160,15 +213,16 @@ InitEnv == (* May change either of the store of chain A or B. - - TODO: Unclear what condition we want precisely! *) + *) GoodNextEnv == - \/ chmA!AdvanceChainHeight /\ UNCHANGED storeChainB - \/ chmB!AdvanceChainHeight /\ UNCHANGED storeChainA -\* \/ \E hA \in Heights : chmA!UpdateClient(hA) /\ UNCHANGED storeChainB -\* \/ \E hB \in Heights : chmB!UpdateClient(hB) /\ UNCHANGED storeChainA - \/ chmA!UpdateClient(storeChainB.latestHeight) /\ UNCHANGED storeChainB - \/ chmB!UpdateClient(storeChainA.latestHeight) /\ UNCHANGED storeChainA + \/ /\ chmA!NotMaxHeight + /\ \/ chmA!AdvanceChainHeight + \/ chmA!UpdateClient(storeChainB.latestHeight) + /\ UNCHANGED storeChainB + \/ /\ chmB!NotMaxHeight + /\ \/ chmB!AdvanceChainHeight + \/ chmB!UpdateClient(storeChainA.latestHeight) + /\ UNCHANGED storeChainA (* The environment injects a msg. in the buffer of one of the chains. @@ -203,10 +257,11 @@ NextEnv == CHProtocolDone == - /\ storeChainA.connection.state = "INIT" (* TODO: should be OPEN *) - /\ storeChainB.connection.state = "INIT" + /\ storeChainA.connection.state = "OPEN" + /\ storeChainB.connection.state = "OPEN" /\ UNCHANGED <> + (****************************************************************************** Main spec. The system comprises the connection handshake module & environment. @@ -216,8 +271,8 @@ CHProtocolDone == (* Initializes both chains, attributing to each a chainID as well as a client. *) Init == - /\ \E clientA \in chmA!InitClients : chmA!Init("chainA", clientA) - /\ \E clientB \in chmB!InitClients : chmB!Init("chainB", clientB) + /\ \E clientB \in chmB!InitClients : chmA!Init("chainA", clientB) + /\ \E clientA \in chmA!InitClients : chmB!Init("chainB", clientA) /\ InitEnv @@ -242,10 +297,9 @@ Spec == (* TODO: Unclear how to capture the type of a sequence. *) -\*TypeInvariant == -\* /\ \/ bufChainA = <<>> -\* \/ \A e in bufChainA : e \in ConnectionHandshakeMessages -\* /\ bufChainB \in ConnectionHandshakeMessages +TypeInvariant == + /\ \/ bufChainA \in Seq(ConnectionHandshakeMessages) + \/ bufChainB \in Seq(ConnectionHandshakeMessages) \* Liveness property. @@ -269,6 +323,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Thu May 14 10:01:23 CEST 2020 by adi +\* Last modified Thu May 14 16:36:27 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/README.md b/verification/spec/connection-handshake/README.md new file mode 100644 index 0000000000..107f0d5e9c --- /dev/null +++ b/verification/spec/connection-handshake/README.md @@ -0,0 +1,28 @@ +# IBC Connection Handshake (CH) TLA+ spec + + +This is a high-level TLA+ spec for the IBC Connection Handshake (CH) protocol. +The spec has two modules: + + - `Environment.tla` (main model lives here) + - `ConnectionHandshakeModule.tla` (the spec for the IBC CH module) + + +To run this: + +1. add the two modules in a new specification in the toolbox +2. specify values for constants MaxHeight and MaxBufLen + +Note the assumptions: + +``` +ASSUME MaxHeight > 1 +ASSUME MaxBufLen > 1 +``` + +Typical values could be: `MaxHeight = 5` and `MaxBufLen = 2`. + + +3. add the invariant `ConsistencyInv` and the property (temporal formula) `Termination` + +4. run the model checker. \ No newline at end of file From 5943c9f538c3782409294b348a3fcac3245ffbbd Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Fri, 15 May 2020 10:03:53 +0200 Subject: [PATCH 45/63] Safety invariant breaks upon parallel Init msgs. Some refurbishing If we make ConnectionParameters more general (see the union in ConnectionParameters.remoteEnd and localEnd) then both chains could initialize and lock on different connections. --- .../ConnectionHandshakeModule.tla | 63 +++----- .../spec/connection-handshake/Environment.tla | 145 +++++++++++------- 2 files changed, 111 insertions(+), 97 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index ceaa1de2a9..c0cb0160a6 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -3,22 +3,23 @@ (*************************************************************************** This module is part of the TLA+ specification for the - IBC Connection Handshake (CH) protocol. + IBC Connection Handshake protocol (ICS3). - This module captures the actions and operators of the IBC CH protocol. + This module captures the actions and operators of the ICS3 protocol. Typically, it is an IBC module running on a chain that would implement - this logic, hence the name "ConnectionHandshakeModule". + the logic in this TLA+ module, hence the name "ConnectionHandshakeModule" + sometimes abbreviated to "chModule" or "chm". - This module deals with a high-level spec of the CH protocol, hence it is - a simplification in several regards: - - the modules assumes to run on a chain which modeled as a simple + This module deals with a high-level spec of the ICS3 protocol, so it is + a simplification with respect to ICS3 proper in several regards: + - the modules assumes to run on a chain which we model as a simple advancing height, plus a few more critical fields (see the 'store'), but without any state (e.g., blockchain, transactions, consensus core); - we model a single connection; establishing multiple connections is not possible; - - we do not do any cryptographic proofs or proof verifications. + - we do not perform any cryptographic proofs or proof verifications. - the abstractions we use are higher-level, and slightly different from - the ones in ICS 003 (see e.g., ConnectionEnd and Connection) + the ones in ICS3 (see e.g., ConnectionEnd and Connection records). - the client colocated with the module is simplified, comprising only a set of heights. @@ -28,8 +29,8 @@ EXTENDS Naturals, FiniteSets, Sequences CONSTANTS MaxHeight, \* Maximum height of the local chain. - ConnectionIDs, \* The set of all possible connection IDs. - ClientIDs, \* The set of all possible client IDs. + ConnectionIDs, \* The set of valid connection IDs. + ClientIDs, \* The set of valid client IDs. MaxBufLen, \* Maximum length of the input and output buffers. ConnectionStates \* All the possible connection states. @@ -118,22 +119,6 @@ InitClients == ] -(******************************* CHMessageTypes ***************************** - - The set of valid message types that this module can handle, e.g., as - incoming or outgoing messages. - - For a complete description of the message record, see - 'Environment.Messages'. - - ***************************************************************************) -CHMessageTypes == - {"CHMsgInit", - "CHMsgTry", - "CHMsgAck", - "CHMsgConfirm"} - - (*************************************************************************** Helper operators. ***************************************************************************) @@ -233,7 +218,7 @@ NewStore(newCon) == !.latestHeight = @ + 1] -(* Handles a "CHMsgInit" message 'm'. +(* Handles a "ICS3MsgInit" message 'm'. Primes the store.connection to become initialized with the parameters specified in 'm'. Also creates a reply message, enqueued on the outgoing @@ -245,7 +230,7 @@ HandleInitMsg(m) == sProof == GetConnProof("INIT") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgTry", + type |-> "ICS3MsgTry", connProof |-> sProof, clientProof |-> cProof] IN IF /\ ValidConnectionParameters(m.parameters) @@ -271,7 +256,7 @@ PreconditionsTryMsg(m) == /\ VerifyClientProof(m.clientProof) -(* Handles a "CHMsgTry" message. +(* Handles a "ICS3MsgTry" message. *) HandleTryMsg(m) == LET newCon == [parameters |-> m.parameters, @@ -279,7 +264,7 @@ HandleTryMsg(m) == sProof == GetConnProof("TRYOPEN") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgAck", + type |-> "ICS3MsgAck", connProof |-> sProof, clientProof |-> cProof] IN IF PreconditionsTryMsg(m) @@ -299,7 +284,7 @@ PreconditionsAckMsg(m) == /\ VerifyConnProof(m.connProof) -(* Handles a "CHMsgAck" message. +(* Handles a "ICS3MsgAck" message. *) HandleAckMsg(m) == LET newCon == [parameters |-> m.parameters, @@ -307,7 +292,7 @@ HandleAckMsg(m) == sProof == GetConnProof("OPEN") cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), - type |-> "CHMsgConfirm", + type |-> "ICS3MsgConfirm", connProof |-> sProof, clientProof |-> cProof] IN IF PreconditionsAckMsg(m) @@ -325,7 +310,7 @@ PreconditionsConfirmMsg(m) == /\ m.connProof.connectionState = "OPEN" /\ VerifyConnProof(m.connProof) -(* Handles a "CHMsgConfirm" message. +(* Handles a "ICS3MsgConfirm" message. *) HandleConfirmMsg(m) == LET newCon == [parameters |-> m.parameters, @@ -368,16 +353,16 @@ UpdateClient(height) == disjunctions will always enable. *) ProcessMsg(m) == - LET res == CASE m.type = "CHMsgInit" -> HandleInitMsg(m) - [] m.type = "CHMsgTry" -> HandleTryMsg(m) - [] m.type = "CHMsgAck" -> HandleAckMsg(m) - [] m.type = "CHMsgConfirm" -> HandleConfirmMsg(m) IN + LET res == CASE m.type = "ICS3MsgInit" -> HandleInitMsg(m) + [] m.type = "ICS3MsgTry" -> HandleTryMsg(m) + [] m.type = "ICS3MsgAck" -> HandleAckMsg(m) + [] m.type = "ICS3MsgConfirm" -> HandleConfirmMsg(m) IN /\ outBuf' = res.out /\ store' = res.store (*************************************************************************** - Connection Handshake Module main spec. + Connection Handshake Module (ICS3) main spec. ***************************************************************************) @@ -402,6 +387,6 @@ Next == ============================================================================= \* Modification History -\* Last modified Thu May 14 16:30:38 CEST 2020 by adi +\* Last modified Fri May 15 09:47:47 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 08bcc13bc1..5a79736632 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -2,8 +2,8 @@ (*************************************************************************** - This module is part of the TLA+ specification for the - IBC Connection Handshake (CH) protocol. This is a high-level spec of CH. + This module is part of the TLA+ specification for the IBC Connection + Handshake protocol (identifier 'ICS3'). This is a high-level spec of ICS3. This module captures the operators and actions outside of the CH protocol itself (i.e., the environment). @@ -11,12 +11,12 @@ - creates two instances of ConnectionHandshakeModule, - wires these instances together, - simulates some malicious behavior by injecting incorrect messages - - provides the initialization step for CH protocol, concretely a - "CHMsgInit" message, so that the two instances can perform the protocol. + - provides the initialization step for ICS3 protocol, concretely a + "ICS3MsgInit" message, so that the two instances can perform the protocol. - updates the clients on each instance periodically, or advances the chain of each instance. - ***************************************************************************) + ***************************************************************************) EXTENDS Naturals, FiniteSets, Sequences @@ -38,12 +38,12 @@ VARIABLES chainAParameters == [ connectionIDs |-> { "connAtoB" }, - clientIDs |-> { "clientIDChainA" } + clientIDs |-> { "clientIDChainB" } ] chainBParameters == [ connectionIDs |-> { "connBtoA" }, - clientIDs |-> { "clientIDChainB" } + clientIDs |-> { "clientIDChainA" } ] @@ -79,30 +79,40 @@ chmA == INSTANCE ConnectionHandshakeModule chmB == INSTANCE ConnectionHandshakeModule WITH MaxHeight <- MaxHeight, - inBuf <- bufChainB, (* Flip the message buffers w.r.t. chain A buffers. *) - outBuf <- bufChainA, (* Inbound for "A" is outbound for "B". *) + inBuf <- bufChainB, (* Flip message buffers *) + outBuf <- bufChainA, (* Inbound of "A" is outbound of "B". *) store <- storeChainB, ConnectionIDs <- chainBParameters.connectionIDs, ClientIDs <- chainBParameters.clientIDs -(* TODO BLOCK COMMENTS *) +(******************************* ConnectionParameters ********************** + A set of connection parameter records. + A connection parameter record contains the following fields: + + - localEnd -- a connection end + Specifies the local connection details (i.e., connection ID and + client ID). + + - remoteEnd -- a connection end + Specifies the local connection details. + + ***************************************************************************) ConnectionParameters == [ - localEnd : chmA!ConnectionEnds, - remoteEnd : chmB!ConnectionEnds + localEnd : chmA!ConnectionEnds \union chmB!ConnectionEnds, + remoteEnd : chmA!ConnectionEnds \union chmB!ConnectionEnds ] (******************************* Connections ******************************* - A set of connection end records. - A connection end record contains the following fields: + A set of connection records. + A connection record contains the following fields: - - connectionID -- a string - Stores the identifier of this connection, specific to a chain. + - parameters -- a connection parameters record + Specifies the local plus remote ends. - - clientID -- a string - Stores the identifier of the client running on this chain. + - state -- a connection state (see ConnectionStates set). ***************************************************************************) Connections == @@ -152,45 +162,65 @@ ClientProofs == Heights == 1..MaxHeight -(******************************** Messages ******************************** -Messages are connection handshake specific. - - The valid message types are defined in: - ConnectionHandshakeModule.CHMessageTypes. - -In the low-level connection handshake protocol, the four messages have the -following types: ConnOpenInit, ConnOpenTry, ConnOpenAck, ConnOpenConfirm. - These are described in ICS 003. - In this high-level specification, we choose slightly different names, to - make an explicit distinction to the low-level protocol. Message types - are as follows: CHMsgInit, CHMsgTry, CHMsgAck, and CHMsgConfirm. Notice that - the fields of each message are also different to the ICS 003 specification. - +(******************************* ICS3MessageTypes ***************************** + + The set of valid message types that the ConnectionHandshakeModule can + handle, e.g., as incoming or outgoing messages. + + In the low-level connection handshake protocol, the four messages have + types: ConnOpenInit, ConnOpenTry, ConnOpenAck, ConnOpenConfirm. + In this high-level specification, we choose slightly different names, to + make an explicit distinction to the low-level protocol. Message types + are as follows: + ICS3MsgInit, ICS3MsgTry, ICS3MsgAck, and ICS3MsgConfirm. + For a complete description of the message record, see + ConnectionHandshakeMessage below. + + ***************************************************************************) +ICS3MessageTypes == + {"ICS3MsgInit", + "ICS3MsgTry", + "ICS3MsgAck", + "ICS3MsgConfirm"} + + +(*********************** ConnectionHandshakeMessages *********************** + + The set of ConnectionHandshakeMessage records. + These are connection handshake specific messages that two chains exchange + while executing the ICS3 protocol. ***************************************************************************) ConnectionHandshakeMessages == - [type : {"CHMsgInit"}, + [type : {"ICS3MsgInit"}, parameters : ConnectionParameters] \union - [type : {"CHMsgTry"}, + [type : {"ICS3MsgTry"}, parameters : ConnectionParameters, connProof : ConnProofs, clientProof : ClientProofs] \union - [type : {"CHMsgAck"}, + [type : {"ICS3MsgAck"}, parameters : ConnectionParameters, connProof : ConnProofs, clientProof : ClientProofs] \union - [type : {"CHMsgConfirm"}, + [type : {"ICS3MsgConfirm"}, parameters : ConnectionParameters, connProof : ConnProofs] -(* The set of all Init messages, such that the local end is the - set 'le', and the remote end is set 're'. *) +(***************************** InitMsgs *********************************** + + The set of ConnectionHandshakeMessage records where message type is + ICS3MsgInit. + + This operator returns the set of all initialization messages, such that + the local end is the set 'le', and the remote end is set 're'. + + ***************************************************************************) InitMsgs(le, re) == - [type : {"CHMsgInit"}, + [type : {"ICS3MsgInit"}, parameters : [localEnd : le, remoteEnd : re]] @@ -226,13 +256,13 @@ GoodNextEnv == (* The environment injects a msg. in the buffer of one of the chains. - This interferes with the CH protocol in two ways: - 1. by introducing additional messages that are incorrect, - 2. by dropping correct messages (overwritting them). + This interferes with the ICS3 protocol by introducing additional + messages that are usually incorrect. - Without the first constraint, on the "Len(bufChainA)" and "Len(bufChainB)", - Env could fill buffers (DoS attack). This can lead to a deadlock, because - chains will simply be unable to reply to each other. + Without the first constraint, on the "Len(bufChainA)" and + "Len(bufChainB)", Env could fill buffers (DoS attack). This can + lead to a deadlock, because chains will simply be unable to reply + to each other. *) MaliciousNextEnv == \/ /\ Len(bufChainA) < MaxBufLen - 1 @@ -256,7 +286,7 @@ NextEnv == /\ UNCHANGED<> -CHProtocolDone == +ICS3ProtocolDone == /\ storeChainA.connection.state = "OPEN" /\ storeChainB.connection.state = "OPEN" /\ UNCHANGED <> @@ -268,17 +298,16 @@ CHProtocolDone == *****************************************************************************) -(* Initializes both chains, attributing to each a chainID as well as a client. - *) +(* Initializes both chains, attributing to each a chainID and a client. *) Init == - /\ \E clientB \in chmB!InitClients : chmA!Init("chainA", clientB) - /\ \E clientA \in chmA!InitClients : chmB!Init("chainB", clientA) + /\ \E clientA \in chmA!InitClients : chmA!Init("chainA", clientA) + /\ \E clientB \in chmB!InitClients : chmB!Init("chainB", clientB) /\ InitEnv -\* The two CH modules and the environment alternate their steps. +(* The two ICS3 modules and the environment alternate their steps. *) Next == - \/ CHProtocolDone + \/ ICS3ProtocolDone \/ NextEnv \/ chmA!Next /\ UNCHANGED <> \/ chmB!Next /\ UNCHANGED <> @@ -296,25 +325,25 @@ Spec == /\ FairProgress -(* TODO: Unclear how to capture the type of a sequence. *) TypeInvariant == /\ \/ bufChainA \in Seq(ConnectionHandshakeMessages) \/ bufChainB \in Seq(ConnectionHandshakeMessages) -\* Liveness property. +(* Liveness property. *) Termination == <> ~ maliciousEnv => <> /\ storeChainA.connection.state = "OPEN" /\ storeChainB.connection.state = "OPEN" -\* Safety property. -\* If the connections in the two chains are not null, then the -\* connection parameters must always match. +(* Safety property. *) ConsistencyInv == \/ storeChainA.connection = chmA!nullConnection \/ storeChainB.connection = chmB!nullConnection + (* If the connections in the two chains are not null, then the + connection parameters must always match. + *) \/ storeChainA.connection.parameters = chmB!FlipConnectionParameters(storeChainB.connection.parameters) @@ -323,6 +352,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Thu May 14 16:36:27 CEST 2020 by adi +\* Last modified Fri May 15 09:50:23 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 6d8b85d946ac35a8c1c1efc28b2cdb3f59c2903c Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Fri, 15 May 2020 13:07:52 +0200 Subject: [PATCH 46/63] Env constrained on Init msgs after sync w/ Anca. Model checks. --- .../ConnectionHandshakeModule.tla | 14 +++--- .../spec/connection-handshake/Environment.tla | 50 ++++++++++--------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index c0cb0160a6..b5a7c29ef6 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -325,11 +325,13 @@ HandleConfirmMsg(m) == (* If MaxHeight is not yet reached, then advance the latestHeight of the chain. *) AdvanceChainHeight == - /\ store' = [store EXCEPT !.latestHeight = @ + 1] + store' = [store EXCEPT !.latestHeight = @ + 1] -NotMaxHeight == - store.latestHeight <= MaxHeight +(* State predicate returning true if MaxHeight not yet attained. + *) +CanProgress == + store.latestHeight < MaxHeight (* Action for updating the local client on this chain with a new height. @@ -375,18 +377,18 @@ Init(chainID, client) == Next == \/ /\ inBuf # <<>> \* Enabled if we have an inbound msg. + /\ store.latestHeight < MaxHeight /\ ProcessMsg(Head(inBuf)) \* Generic action for handling a msg. /\ inBuf' = Tail(inBuf) \* Strip the head of our inbound msg. buffer. \/ /\ inBuf = <<>> + /\ store.latestHeight = MaxHeight /\ UNCHANGED<> -\* /\ store.latestHeight < MaxHeight -\* /\ store.latestHeight = MaxHeight ============================================================================= \* Modification History -\* Last modified Fri May 15 09:47:47 CEST 2020 by adi +\* Last modified Fri May 15 12:55:45 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 5a79736632..bac7032ed0 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -96,12 +96,12 @@ chmB == INSTANCE ConnectionHandshakeModule - remoteEnd -- a connection end Specifies the local connection details. - + ***************************************************************************) ConnectionParameters == [ - localEnd : chmA!ConnectionEnds \union chmB!ConnectionEnds, - remoteEnd : chmA!ConnectionEnds \union chmB!ConnectionEnds + localEnd : chmA!ConnectionEnds, + remoteEnd : chmB!ConnectionEnds ] @@ -210,6 +210,8 @@ ConnectionHandshakeMessages == connProof : ConnProofs] + + (***************************** InitMsgs *********************************** The set of ConnectionHandshakeMessage records where message type is @@ -231,25 +233,26 @@ InitMsgs(le, re) == (* Assigns a sequence with 1 element -- the Init msg -- to either bufChainA - or bufChainB. *) + or bufChainB. + + TODO: Maybe reduce this. + *) InitEnv == /\ maliciousEnv = TRUE - /\ \/ /\ bufChainA \in {<> : - msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} - /\ bufChainB = <<>> (* Empty buffer initially. *) - \/ /\ bufChainB \in {<> : - msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} - /\ bufChainA = <<>> + /\ bufChainA \in {<> : + msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} + /\ bufChainB \in {<> : + msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} (* May change either of the store of chain A or B. *) GoodNextEnv == - \/ /\ chmA!NotMaxHeight - /\ \/ chmA!AdvanceChainHeight + \/ /\ chmA!CanProgress + /\ \/ chmA!AdvanceChainHeight \/ chmA!UpdateClient(storeChainB.latestHeight) /\ UNCHANGED storeChainB - \/ /\ chmB!NotMaxHeight + \/ /\ chmB!CanProgress /\ \/ chmB!AdvanceChainHeight \/ chmB!UpdateClient(storeChainA.latestHeight) /\ UNCHANGED storeChainA @@ -316,7 +319,7 @@ Next == FairProgress == /\ WF_chainAVars(chmA!Next) /\ WF_chainBVars(chmB!Next) - /\ WF_chainStoreVars(GoodNextEnv) +\* /\ WF_chainStoreVars(GoodNextEnv) Spec == @@ -331,27 +334,28 @@ TypeInvariant == (* Liveness property. *) +(* As long as all chains CanProgress: We should reach open & open. *) Termination == - <> ~ maliciousEnv - => <> /\ storeChainA.connection.state = "OPEN" - /\ storeChainB.connection.state = "OPEN" + <> [](chmA!CanProgress /\ chmB!CanProgress) + => [](/\ storeChainA.connection.state = "OPEN" + /\ storeChainB.connection.state = "OPEN") (* Safety property. *) -ConsistencyInv == - \/ storeChainA.connection = chmA!nullConnection - \/ storeChainB.connection = chmB!nullConnection +ConsistencyProperty == + \/ storeChainA.connection.state = "OPEN" + \/ storeChainB.connection.state = "OPEN" (* If the connections in the two chains are not null, then the connection parameters must always match. *) - \/ storeChainA.connection.parameters + => storeChainA.connection.parameters = chmB!FlipConnectionParameters(storeChainB.connection.parameters) Consistency == - [] ConsistencyInv + [] ConsistencyProperty ============================================================================= \* Modification History -\* Last modified Fri May 15 09:50:23 CEST 2020 by adi +\* Last modified Fri May 15 13:01:37 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From de326c70dcc6da47e7c296c1be3a550ec11f7e42 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Mon, 18 May 2020 14:31:45 +0200 Subject: [PATCH 47/63] Documented the init message deadlock issue. Made the malicious env. more powerful. Adjusted the properties. --- .../spec/connection-handshake/Environment.tla | 143 ++++++++++++++---- 1 file changed, 117 insertions(+), 26 deletions(-) diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index bac7032ed0..f1f51647ca 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -100,8 +100,8 @@ chmB == INSTANCE ConnectionHandshakeModule ***************************************************************************) ConnectionParameters == [ - localEnd : chmA!ConnectionEnds, - remoteEnd : chmB!ConnectionEnds + localEnd : chmA!ConnectionEnds \union chmB!ConnectionEnds, + remoteEnd : chmB!ConnectionEnds \union chmA!ConnectionEnds ] @@ -192,7 +192,7 @@ ICS3MessageTypes == ***************************************************************************) ConnectionHandshakeMessages == - [type : {"ICS3MsgInit"}, + [type : {"ICS3MsgInit"}, parameters : ConnectionParameters] \union [type : {"ICS3MsgTry"}, @@ -210,8 +210,6 @@ ConnectionHandshakeMessages == connProof : ConnProofs] - - (***************************** InitMsgs *********************************** The set of ConnectionHandshakeMessage records where message type is @@ -232,20 +230,73 @@ InitMsgs(le, re) == ***************************************************************************) -(* Assigns a sequence with 1 element -- the Init msg -- to either bufChainA - or bufChainB. +(* Environment initialization. + + This action kick-stars the ICS3 protocol by assigning an ICS3MsgInit + msg to either of the two chains (or both). + + Initially, the environment is non-malicious. The environment starts + acting maliciously once the connection on both chains transitions out + of state "UNINIT" (the initials state). It is important to + initialize the protocol like this, otherwise the env. can provoke a + deadlock. This can happen with the following sequence of actions: + + 1. Environment injects a ICS3MsgInit to chain A with the following + correct parameters: + + localEnd |-> [connectionID |-> "connAtoB", + clientID |-> "clientIDChainB"] + remoteEnd |-> [connectionID |-> "connBtoA", + clientID |-> "clientIDChainA"] + + 2. Environment injects, maliciously, a ICS3MsgInit to chain B with + the following parameters: + + localEnd |-> [connectionID |-> "connBtoA", + clientID |-> "clientIDChainA"] + remoteEnd |-> [connectionID |-> "connBtoA", + clientID |-> "clientIDChainA"] + + Notice that the localEnd is correct, so chain B will validate and + process this message; the remoteEnd is incorrect, however, but chain + B is not able to validate that part of the connection, so it will + accept it as it is. + + 2. Chain A processes the ICS3MsgInit (action HandleInitMsg) and + updates its store.connection with the parameters from step 1 above. + At this point, chain A "locks onto" these parameters and will not + accept any others. Chain A also produces a ICS3MsgTry message. + + 3. Chain B processes the ICS3MsgInit (action HandleInitMsg) and + updates its store.connection with the parameters from step 2 above. + Chain B "locks onto" these parameters and will not accept any others. + At this step, chain B produces a ICS3MsgTry message with the local + parameters from its connection. + + Both chains will be locked on a different set of connection parameters, + and neither chain will accept their corresponding ICS3MsgTry, hence a + deadlock. To avoid this problem, we prevent the environment from + acting maliciously in the preliminary parts of the ICS3 protocol, until + both chains finish locking on the same set of connection parameters. - TODO: Maybe reduce this. *) InitEnv == - /\ maliciousEnv = TRUE - /\ bufChainA \in {<> : + /\ maliciousEnv = FALSE + /\ \/ /\ bufChainA \in {<> : (* ICS3MsgInit to chain A. *) + msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} + /\ bufChainB = <<>> + \/ /\ bufChainB \in {<> : (* ICS3MsgInit to chain B. *) + msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} + /\ bufChainB = <<>> + \/ /\ bufChainA \in {<> : (* ICS3MsgInit to both chains. *) msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} - /\ bufChainB \in {<> : + /\ bufChainB \in {<> : msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} -(* May change either of the store of chain A or B. +(* Default next (good) action for Environment. + + May change either of the store of chain A or B. *) GoodNextEnv == \/ /\ chmA!CanProgress @@ -258,7 +309,9 @@ GoodNextEnv == /\ UNCHANGED storeChainA -(* The environment injects a msg. in the buffer of one of the chains. +(* Environment malicious behavior. + + The environment injects a msg. in the buffer of one of the chains. This interferes with the ICS3 protocol by introducing additional messages that are usually incorrect. @@ -278,23 +331,61 @@ MaliciousNextEnv == /\ UNCHANGED bufChainA + +(* Environment next action. + + There are four possible actions that the environment may perform: + + 1. A good step: the environment advances or updates the client on + one of the two chains. + + 2. The environment becomes malicious, as a result of both chains + advancing past the UNINIT step (i.e., after both chains finished + locking on to a set of connection parameters). + + 3. A malicious step. + + 4. The environment stops acting maliciously. + *) NextEnv == - \/ /\ GoodNextEnv + \/ /\ GoodNextEnv (* A good step. *) /\ UNCHANGED<> - \/ /\ maliciousEnv = TRUE + \/ /\ maliciousEnv = FALSE (* Enable malicious env. *) + /\ storeChainA.connection.state # "UNINIT" + /\ storeChainB.connection.state # "UNINIT" + /\ maliciousEnv' = TRUE + /\ MaliciousNextEnv + /\ UNCHANGED chainStoreVars + \/ /\ maliciousEnv = TRUE (* A malicious step. *) /\ MaliciousNextEnv /\ UNCHANGED<> - \/ /\ maliciousEnv = TRUE + \/ /\ maliciousEnv = TRUE (* Disable malicious env. *) /\ maliciousEnv' = FALSE /\ UNCHANGED<> -ICS3ProtocolDone == +(* Enables when the connection is open on both chains. + + State predicate signaling that the protocol terminated correctly. + *) +ICS3ReachTermination == /\ storeChainA.connection.state = "OPEN" /\ storeChainB.connection.state = "OPEN" /\ UNCHANGED <> +(* Enables when both chains attain maximum height, if the connection is still + not opened. + + State predicate signaling that the chains cannot progress any further, + and therefore the protocol terminates without successfully opening the + connection. + *) +ICS3NonTermination == + /\ (~ chmA!CanProgress \/ storeChainA.connection.state # "OPEN") + /\ (~ chmB!CanProgress \/ storeChainB.connection.state # "OPEN") + /\ UNCHANGED <> + (****************************************************************************** Main spec. The system comprises the connection handshake module & environment. @@ -310,7 +401,8 @@ Init == (* The two ICS3 modules and the environment alternate their steps. *) Next == - \/ ICS3ProtocolDone + \/ ICS3ReachTermination + \/ ICS3NonTermination \/ NextEnv \/ chmA!Next /\ UNCHANGED <> \/ chmB!Next /\ UNCHANGED <> @@ -319,7 +411,6 @@ Next == FairProgress == /\ WF_chainAVars(chmA!Next) /\ WF_chainBVars(chmB!Next) -\* /\ WF_chainStoreVars(GoodNextEnv) Spec == @@ -334,17 +425,17 @@ TypeInvariant == (* Liveness property. *) -(* As long as all chains CanProgress: We should reach open & open. *) +(* If both chains CanProgress: We should reach open & open. *) Termination == - <> [](chmA!CanProgress /\ chmB!CanProgress) - => [](/\ storeChainA.connection.state = "OPEN" - /\ storeChainB.connection.state = "OPEN") + [](chmA!CanProgress /\ chmB!CanProgress) + => <> [](/\ storeChainA.connection.state = "OPEN" + /\ storeChainB.connection.state = "OPEN") (* Safety property. *) ConsistencyProperty == - \/ storeChainA.connection.state = "OPEN" - \/ storeChainB.connection.state = "OPEN" + /\ storeChainA.connection.state # "UNINIT" + /\ storeChainB.connection.state # "UNINIT" (* If the connections in the two chains are not null, then the connection parameters must always match. *) @@ -356,6 +447,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Fri May 15 13:01:37 CEST 2020 by adi +\* Last modified Mon May 18 14:28:24 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From cf70dd3c00bcd43a9a5475213f12b3388089a198 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Mon, 18 May 2020 15:22:00 +0200 Subject: [PATCH 48/63] Consistent comment notation --- .../ConnectionHandshakeModule.tla | 26 +++++++++---------- .../spec/connection-handshake/Environment.tla | 22 +++++++++------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index b5a7c29ef6..c48bd1f7c9 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -60,8 +60,10 @@ VARIABLES ***************************************************************************) store, - inBuf, \* A buffer (Sequence) holding any message(s) incoming to the chModule. - outBuf \* A buffer (Sequence) holding outbound message(s) from the chModule. + (* A buffer (Sequence) holding any message(s) incoming to this module. *) + inBuf, + (* A buffer (Sequence) holding outbound message(s) from this module. *) + outBuf moduleVars == <> @@ -210,8 +212,7 @@ VerifyClientProof(cp) == (* Modifies the local store. Replaces the connection in the store with the argument 'newCon'. - If the height (latestHeight) of the chain has not yet attained MaxHeight, this action also - advances the chain height. + This action also advances the chain height. *) NewStore(newCon) == [store EXCEPT !.connection = newCon, @@ -322,7 +323,9 @@ HandleConfirmMsg(m) == store |-> store] -(* If MaxHeight is not yet reached, then advance the latestHeight of the chain. +(* Action for advancing the current height (latestHeight) of the chain. + + The environment triggers this as part of the GoodNextEnv action. *) AdvanceChainHeight == store' = [store EXCEPT !.latestHeight = @ + 1] @@ -334,12 +337,11 @@ CanProgress == store.latestHeight < MaxHeight -(* Action for updating the local client on this chain with a new height. +(* Action for updating the local client on this chain with a height. - This may prime the store; leaves the chain buffers unchanged. - This will also advance the chain height unless MaxHeight is reached. - If the 'height' parameter already exists as part of - 'store.client.consensusStates', then we do not change the store. + The environment triggers this as part of the GoodNextEnv action. + This primes the store; leaves the chain buffers unchanged. + This will also advance the chain height. *) UpdateClient(height) == store' = [store EXCEPT !.latestHeight = @ + 1, @@ -385,10 +387,8 @@ Next == /\ UNCHANGED<> - - ============================================================================= \* Modification History -\* Last modified Fri May 15 12:55:45 CEST 2020 by adi +\* Last modified Mon May 18 15:08:56 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index f1f51647ca..257493ca94 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -261,7 +261,7 @@ InitMsgs(le, re) == process this message; the remoteEnd is incorrect, however, but chain B is not able to validate that part of the connection, so it will accept it as it is. - + 2. Chain A processes the ICS3MsgInit (action HandleInitMsg) and updates its store.connection with the parameters from step 1 above. At this point, chain A "locks onto" these parameters and will not @@ -278,7 +278,6 @@ InitMsgs(le, re) == deadlock. To avoid this problem, we prevent the environment from acting maliciously in the preliminary parts of the ICS3 protocol, until both chains finish locking on the same set of connection parameters. - *) InitEnv == /\ maliciousEnv = FALSE @@ -331,7 +330,6 @@ MaliciousNextEnv == /\ UNCHANGED bufChainA - (* Environment next action. There are four possible actions that the environment may perform: @@ -386,6 +384,7 @@ ICS3NonTermination == /\ (~ chmB!CanProgress \/ storeChainB.connection.state # "OPEN") /\ UNCHANGED <> + (****************************************************************************** Main spec. The system comprises the connection handshake module & environment. @@ -424,21 +423,24 @@ TypeInvariant == \/ bufChainB \in Seq(ConnectionHandshakeMessages) -(* Liveness property. *) -(* If both chains CanProgress: We should reach open & open. *) +(* Liveness property. + + If both chains can progress, we should reach open on both chains. +*) Termination == [](chmA!CanProgress /\ chmB!CanProgress) => <> [](/\ storeChainA.connection.state = "OPEN" /\ storeChainB.connection.state = "OPEN") -(* Safety property. *) +(* Safety property. + + If the connections in the two chains are not null, then the + connection parameters must always match. + *) ConsistencyProperty == /\ storeChainA.connection.state # "UNINIT" /\ storeChainB.connection.state # "UNINIT" - (* If the connections in the two chains are not null, then the - connection parameters must always match. - *) => storeChainA.connection.parameters = chmB!FlipConnectionParameters(storeChainB.connection.parameters) @@ -447,6 +449,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Mon May 18 14:28:24 CEST 2020 by adi +\* Last modified Mon May 18 15:11:58 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 7cc29336d62e48611d0039d481467ba44c2043ec Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 19 May 2020 10:07:32 +0200 Subject: [PATCH 49/63] Fixed ACK handler bug; factored out common declarations into a separate module ICS3Types.tla; added type invariants. --- .../ConnectionHandshakeModule.tla | 96 +++----- .../spec/connection-handshake/Environment.tla | 211 ++++-------------- 2 files changed, 71 insertions(+), 236 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index c48bd1f7c9..a817ef9fce 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -25,14 +25,14 @@ ***************************************************************************) -EXTENDS Naturals, FiniteSets, Sequences +EXTENDS Naturals, FiniteSets, Sequences, ICS3Types -CONSTANTS MaxHeight, \* Maximum height of the local chain. +CONSTANTS MaxChainHeight, \* Maximum height of the local chain. ConnectionIDs, \* The set of valid connection IDs. ClientIDs, \* The set of valid client IDs. - MaxBufLen, \* Maximum length of the input and output buffers. - ConnectionStates \* All the possible connection states. + MaxBufLen \* Maximum length of the input and output buffers. + ASSUME Cardinality(ConnectionIDs) >= 1 ASSUME Cardinality(ClientIDs) >= 1 @@ -69,58 +69,6 @@ VARIABLES moduleVars == <> -(******************************* ConnectionEnds ***************************** - A set of connection end records. - A connection end record contains the following fields: - - - connectionID -- a string - Stores the identifier of this connection, specific to a chain. - - - clientID -- a string - Stores the identifier of the client running on this chain. - - ***************************************************************************) -ConnectionEnds == - [ - connectionID : ConnectionIDs, - clientID : ClientIDs - ] - - -(* - Initially, the connection on this chain is uninitialized. -*) -nullConnection == [state |-> "UNINIT"] - - -(******************************* InitClients ***************************** - A set of records describing the possible initial values for the - clients on this chain. - - A client record contains the following fields: - - - consensusStates -- a set of heights, each height being a Nat - Stores the set of all heights (i.e., consensus states) that this - client observed. At initialization time, the client only observes - the first height, so the only possible value for this record is - {1}. - - - clientID -- a string - The identifier of the client. - - - latestHeight -- a natural number - Stores the latest height among all the heights in consensusStates. - Initialized to 1. - - ***************************************************************************) -InitClients == - [ - consensusStates : {{1}}, - clientID : ClientIDs, - latestHeight : {1} - ] - - (*************************************************************************** Helper operators. ***************************************************************************) @@ -150,8 +98,8 @@ CheckLocalParameters(para) == ValidConnectionParameters(para) == /\ para.localEnd.connectionID \in ConnectionIDs /\ para.localEnd.clientID \in ClientIDs - /\ \/ store.connection = nullConnection - \/ /\ store.connection /= nullConnection + /\ \/ store.connection = NullConnection + \/ /\ store.connection /= NullConnection /\ CheckLocalParameters(para) @@ -294,8 +242,7 @@ HandleAckMsg(m) == cProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "ICS3MsgConfirm", - connProof |-> sProof, - clientProof |-> cProof] IN + connProof |-> sProof] IN IF PreconditionsAckMsg(m) THEN [out |-> Append(outBuf, replyMsg), store |-> NewStore(newCon)] @@ -331,10 +278,10 @@ AdvanceChainHeight == store' = [store EXCEPT !.latestHeight = @ + 1] -(* State predicate returning true if MaxHeight not yet attained. +(* State predicate returning true if MaxChainHeight not yet attained. *) -CanProgress == - store.latestHeight < MaxHeight +CanAdvance == + store.latestHeight < MaxChainHeight (* Action for updating the local client on this chain with a height. @@ -344,9 +291,10 @@ CanProgress == This will also advance the chain height. *) UpdateClient(height) == - store' = [store EXCEPT !.latestHeight = @ + 1, - !.client.consensusStates = @ \cup {height}, - !.client.latestHeight = height] + /\ height \notin store.client.consensusStates + /\ store' = [store EXCEPT !.latestHeight = @ + 1, + !.client.consensusStates = @ \cup {height}, + !.client.latestHeight = height] (* Generic action for handling any type of inbound message. @@ -370,25 +318,31 @@ ProcessMsg(m) == ***************************************************************************) -Init(chainID, client) == +Init(chainID, client, connection) == /\ store = [id |-> chainID, latestHeight |-> 1, - connection |-> nullConnection, + connection |-> connection, client |-> client] Next == \/ /\ inBuf # <<>> \* Enabled if we have an inbound msg. - /\ store.latestHeight < MaxHeight + /\ CanAdvance /\ ProcessMsg(Head(inBuf)) \* Generic action for handling a msg. /\ inBuf' = Tail(inBuf) \* Strip the head of our inbound msg. buffer. \/ /\ inBuf = <<>> - /\ store.latestHeight = MaxHeight + /\ ~ CanAdvance /\ UNCHANGED<> +TypeInvariant == + /\ inBuf \in Seq(ConnectionHandshakeMessages) \union {<<>>} + /\ outBuf \in Seq(ConnectionHandshakeMessages) \union {<<>>} + /\ store.connection \in Connections + + ============================================================================= \* Modification History -\* Last modified Mon May 18 15:08:56 CEST 2020 by adi +\* Last modified Tue May 19 09:56:48 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 257493ca94..ccbf60b42b 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -24,6 +24,7 @@ EXTENDS Naturals, FiniteSets, Sequences CONSTANT MaxHeight, \* Maximum height of any chain in the system. MaxBufLen \* Length (size) of message buffers. + ASSUME MaxHeight > 1 ASSUME MaxBufLen > 1 @@ -47,8 +48,8 @@ chainBParameters == [ ] -ClientIDs == { chainAParameters.clientIDs, chainBParameters.clientIDs } -ConnectionIDs == { chainAParameters.connectionIDs, chainBParameters.connectionIDs } +AllClientIDs == chainAParameters.clientIDs \union chainBParameters.clientIDs +AllConnectionIDs == chainAParameters.connectionIDs \union chainBParameters.connectionIDs (* Bundle with variables that chain A has access to. *) @@ -66,10 +67,12 @@ chainStoreVars == <> allVars == <> -ConnectionStates == {"UNINIT", "INIT", "TRYOPEN", "OPEN"} + +INSTANCE ICS3Types + WITH MaxHeight <- MaxHeight chmA == INSTANCE ConnectionHandshakeModule - WITH MaxHeight <- MaxHeight, + WITH MaxChainHeight <- MaxHeight, inBuf <- bufChainA, outBuf <- bufChainB, store <- storeChainA, @@ -78,7 +81,7 @@ chmA == INSTANCE ConnectionHandshakeModule chmB == INSTANCE ConnectionHandshakeModule - WITH MaxHeight <- MaxHeight, + WITH MaxChainHeight <- MaxHeight, inBuf <- bufChainB, (* Flip message buffers *) outBuf <- bufChainA, (* Inbound of "A" is outbound of "B". *) store <- storeChainB, @@ -86,145 +89,6 @@ chmB == INSTANCE ConnectionHandshakeModule ClientIDs <- chainBParameters.clientIDs -(******************************* ConnectionParameters ********************** - A set of connection parameter records. - A connection parameter record contains the following fields: - - - localEnd -- a connection end - Specifies the local connection details (i.e., connection ID and - client ID). - - - remoteEnd -- a connection end - Specifies the local connection details. - - ***************************************************************************) -ConnectionParameters == - [ - localEnd : chmA!ConnectionEnds \union chmB!ConnectionEnds, - remoteEnd : chmB!ConnectionEnds \union chmA!ConnectionEnds - ] - - -(******************************* Connections ******************************* - A set of connection records. - A connection record contains the following fields: - - - parameters -- a connection parameters record - Specifies the local plus remote ends. - - - state -- a connection state (see ConnectionStates set). - - ***************************************************************************) -Connections == - [ - parameters : ConnectionParameters, - state : ConnectionStates - ] - - -(******************************* ConnProof ********************************* - A set of records describing the possible values for connection proofs. - - A connection proof record contains the following fields: - - - connectionState -- a string - Captures the state of the connection in the local store of the module - which created this proof. - - - height -- a Nat - The current height (latestHeight) of the chain at the moment when the - module created this proof. - - ***************************************************************************) -ConnProofs == - [ - connectionState : ConnectionStates, - height : 1..MaxHeight - ] - - -(******************************* ClientProofs ******************************* - A set of records describing the possible values for client proofs. - - A client proof record contains the following fields: - - - height -- a Nat - The current height (latestHeight) of the client colocated with module - which created this proof. - - ***************************************************************************) -ClientProofs == - [ - height : 1..MaxHeight - ] - - -Heights == 1..MaxHeight - - -(******************************* ICS3MessageTypes ***************************** - - The set of valid message types that the ConnectionHandshakeModule can - handle, e.g., as incoming or outgoing messages. - - In the low-level connection handshake protocol, the four messages have - types: ConnOpenInit, ConnOpenTry, ConnOpenAck, ConnOpenConfirm. - In this high-level specification, we choose slightly different names, to - make an explicit distinction to the low-level protocol. Message types - are as follows: - ICS3MsgInit, ICS3MsgTry, ICS3MsgAck, and ICS3MsgConfirm. - For a complete description of the message record, see - ConnectionHandshakeMessage below. - - ***************************************************************************) -ICS3MessageTypes == - {"ICS3MsgInit", - "ICS3MsgTry", - "ICS3MsgAck", - "ICS3MsgConfirm"} - - -(*********************** ConnectionHandshakeMessages *********************** - - The set of ConnectionHandshakeMessage records. - These are connection handshake specific messages that two chains exchange - while executing the ICS3 protocol. - - ***************************************************************************) -ConnectionHandshakeMessages == - [type : {"ICS3MsgInit"}, - parameters : ConnectionParameters] - \union - [type : {"ICS3MsgTry"}, - parameters : ConnectionParameters, - connProof : ConnProofs, - clientProof : ClientProofs] - \union - [type : {"ICS3MsgAck"}, - parameters : ConnectionParameters, - connProof : ConnProofs, - clientProof : ClientProofs] - \union - [type : {"ICS3MsgConfirm"}, - parameters : ConnectionParameters, - connProof : ConnProofs] - - -(***************************** InitMsgs *********************************** - - The set of ConnectionHandshakeMessage records where message type is - ICS3MsgInit. - - This operator returns the set of all initialization messages, such that - the local end is the set 'le', and the remote end is set 're'. - - ***************************************************************************) -InitMsgs(le, re) == - [type : {"ICS3MsgInit"}, - parameters : [localEnd : le, - remoteEnd : re]] - - (*************************************************************************** Environment actions. ***************************************************************************) @@ -298,11 +162,11 @@ InitEnv == May change either of the store of chain A or B. *) GoodNextEnv == - \/ /\ chmA!CanProgress + \/ /\ chmA!CanAdvance /\ \/ chmA!AdvanceChainHeight \/ chmA!UpdateClient(storeChainB.latestHeight) /\ UNCHANGED storeChainB - \/ /\ chmB!CanProgress + \/ /\ chmB!CanAdvance /\ \/ chmB!AdvanceChainHeight \/ chmB!UpdateClient(storeChainA.latestHeight) /\ UNCHANGED storeChainA @@ -348,16 +212,16 @@ MaliciousNextEnv == NextEnv == \/ /\ GoodNextEnv (* A good step. *) /\ UNCHANGED<> - \/ /\ maliciousEnv = FALSE (* Enable malicious env. *) - /\ storeChainA.connection.state # "UNINIT" - /\ storeChainB.connection.state # "UNINIT" - /\ maliciousEnv' = TRUE - /\ MaliciousNextEnv - /\ UNCHANGED chainStoreVars - \/ /\ maliciousEnv = TRUE (* A malicious step. *) +\* \/ /\ ~ maliciousEnv (* Enable malicious env. *) +\* /\ storeChainA.connection.state # "UNINIT" +\* /\ storeChainB.connection.state # "UNINIT" +\* /\ maliciousEnv' = TRUE +\* /\ MaliciousNextEnv +\* /\ UNCHANGED chainStoreVars + \/ /\ maliciousEnv (* A malicious step. *) /\ MaliciousNextEnv /\ UNCHANGED<> - \/ /\ maliciousEnv = TRUE (* Disable malicious env. *) + \/ /\ maliciousEnv (* Disable malicious env. *) /\ maliciousEnv' = FALSE /\ UNCHANGED<> @@ -380,8 +244,8 @@ ICS3ReachTermination == connection. *) ICS3NonTermination == - /\ (~ chmA!CanProgress \/ storeChainA.connection.state # "OPEN") - /\ (~ chmB!CanProgress \/ storeChainB.connection.state # "OPEN") + /\ (~ chmA!CanAdvance \/ storeChainA.connection.state # "OPEN") + /\ (~ chmB!CanAdvance \/ storeChainB.connection.state # "OPEN") /\ UNCHANGED <> @@ -393,8 +257,10 @@ ICS3NonTermination == (* Initializes both chains, attributing to each a chainID and a client. *) Init == - /\ \E clientA \in chmA!InitClients : chmA!Init("chainA", clientA) - /\ \E clientB \in chmB!InitClients : chmB!Init("chainB", clientB) + /\ \E clientA \in InitClients(chainAParameters.clientIDs) : + chmA!Init("chainA", clientA, NullConnection) + /\ \E clientB \in InitClients(chainBParameters.clientIDs) : + chmB!Init("chainB", clientB, NullConnection) /\ InitEnv @@ -410,6 +276,10 @@ Next == FairProgress == /\ WF_chainAVars(chmA!Next) /\ WF_chainBVars(chmB!Next) + /\ \A height \in 1..MaxHeight : WF_storeChainA(chmA!UpdateClient(height)) + /\ \A height \in 1..MaxHeight : WF_storeChainB(chmB!UpdateClient(height)) + /\ WF_storeChainA(chmA!AdvanceChainHeight) + /\ WF_storeChainB(chmB!AdvanceChainHeight) Spec == @@ -419,18 +289,28 @@ Spec == TypeInvariant == - /\ \/ bufChainA \in Seq(ConnectionHandshakeMessages) - \/ bufChainB \in Seq(ConnectionHandshakeMessages) + /\ chmA!TypeInvariant + /\ chmB!TypeInvariant (* Liveness property. If both chains can progress, we should reach open on both chains. *) -Termination == - [](chmA!CanProgress /\ chmB!CanProgress) - => <> [](/\ storeChainA.connection.state = "OPEN" - /\ storeChainB.connection.state = "OPEN") +\*Termination == +\*\* [](chmA!CanAdvance /\ chmB!CanAdvance) +\*\* => +\* <> (/\ storeChainA.connection.state = "OPEN" +\* /\ storeChainB.connection.state = "OPEN" +\* /\ bufChainA = <<>> +\* /\ bufChainB = <<>> +\* /\ chmA!CanAdvance +\* /\ chmB!CanAdvance +\* /\ storeChainA.client = storeChainB.client ) + + +MockProperty == + <> /\ storeChainA.latestHeight = 3 (* Safety property. @@ -444,11 +324,12 @@ ConsistencyProperty == => storeChainA.connection.parameters = chmB!FlipConnectionParameters(storeChainB.connection.parameters) + Consistency == [] ConsistencyProperty ============================================================================= \* Modification History -\* Last modified Mon May 18 15:11:58 CEST 2020 by adi +\* Last modified Tue May 19 09:45:49 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From ca02bb72eea7e1b83234207c50418320c9a15366 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 19 May 2020 11:37:23 +0200 Subject: [PATCH 50/63] Fixed local connection end init values --- .../ConnectionHandshakeModule.tla | 3 +- .../spec/connection-handshake/Environment.tla | 56 +++++++++++-------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index a817ef9fce..c096ed8bb5 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -339,10 +339,11 @@ TypeInvariant == /\ inBuf \in Seq(ConnectionHandshakeMessages) \union {<<>>} /\ outBuf \in Seq(ConnectionHandshakeMessages) \union {<<>>} /\ store.connection \in Connections + /\ store.client.clientID \in ClientIDs \union {NullClientID} ============================================================================= \* Modification History -\* Last modified Tue May 19 09:56:48 CEST 2020 by adi +\* Last modified Tue May 19 11:29:24 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index ccbf60b42b..5ff5cf4057 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -37,19 +37,32 @@ VARIABLES maliciousEnv \* If TRUE, environment interferes w/ CH protocol. -chainAParameters == [ - connectionIDs |-> { "connAtoB" }, - clientIDs |-> { "clientIDChainB" } -] +(************* chainAConnectionEnds & chainBConnectionEnds ***************** -chainBParameters == [ - connectionIDs |-> { "connBtoA" }, - clientIDs |-> { "clientIDChainA" } -] + The set of records that each chain can use as a valid local connection + end. For each chain, this set contains one record, since we are + modeling a single connection in this specification. + + ***************************************************************************) +chainAConnectionEnds == + [ + connectionID : { "connAtoB" }, + clientID : { "clientOnAToB" } + ] +chainBConnectionEnds == + [ + connectionID : { "connBtoA" }, + clientID : { "clientOnBToA" } + ] + +AllConnectionEnds == + chainAConnectionEnds \union chainBConnectionEnds +AllClientIDs == + { x.clientID : x \in AllConnectionEnds } -AllClientIDs == chainAParameters.clientIDs \union chainBParameters.clientIDs -AllConnectionIDs == chainAParameters.connectionIDs \union chainBParameters.connectionIDs +AllConnectionIDs == + { x.connectionID : x \in AllConnectionEnds } (* Bundle with variables that chain A has access to. *) @@ -69,15 +82,14 @@ allVars == <> INSTANCE ICS3Types - WITH MaxHeight <- MaxHeight chmA == INSTANCE ConnectionHandshakeModule WITH MaxChainHeight <- MaxHeight, inBuf <- bufChainA, outBuf <- bufChainB, store <- storeChainA, - ConnectionIDs <- chainAParameters.connectionIDs, - ClientIDs <- chainAParameters.clientIDs + ConnectionIDs <- { x.connectionID : x \in chainAConnectionEnds }, + ClientIDs <- { x.clientID : x \in chainAConnectionEnds } chmB == INSTANCE ConnectionHandshakeModule @@ -85,8 +97,8 @@ chmB == INSTANCE ConnectionHandshakeModule inBuf <- bufChainB, (* Flip message buffers *) outBuf <- bufChainA, (* Inbound of "A" is outbound of "B". *) store <- storeChainB, - ConnectionIDs <- chainBParameters.connectionIDs, - ClientIDs <- chainBParameters.clientIDs + ConnectionIDs <- { x.connectionID : x \in chainBConnectionEnds }, + ClientIDs <- { x.clientID : x \in chainBConnectionEnds } (*************************************************************************** @@ -146,15 +158,15 @@ chmB == INSTANCE ConnectionHandshakeModule InitEnv == /\ maliciousEnv = FALSE /\ \/ /\ bufChainA \in {<> : (* ICS3MsgInit to chain A. *) - msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} + msg \in InitMsgs(chainAConnectionEnds, chainBConnectionEnds)} /\ bufChainB = <<>> \/ /\ bufChainB \in {<> : (* ICS3MsgInit to chain B. *) - msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} + msg \in InitMsgs(chainBConnectionEnds, chainAConnectionEnds)} /\ bufChainB = <<>> \/ /\ bufChainA \in {<> : (* ICS3MsgInit to both chains. *) - msg \in InitMsgs(chmA!ConnectionEnds, chmB!ConnectionEnds)} + msg \in InitMsgs(chainAConnectionEnds, chainBConnectionEnds)} /\ bufChainB \in {<> : - msg \in InitMsgs(chmB!ConnectionEnds, chmA!ConnectionEnds)} + msg \in InitMsgs(chainBConnectionEnds, chainAConnectionEnds)} (* Default next (good) action for Environment. @@ -257,9 +269,9 @@ ICS3NonTermination == (* Initializes both chains, attributing to each a chainID and a client. *) Init == - /\ \E clientA \in InitClients(chainAParameters.clientIDs) : + /\ \E clientA \in InitClients({ x.clientID : x \in chainAConnectionEnds }) : chmA!Init("chainA", clientA, NullConnection) - /\ \E clientB \in InitClients(chainBParameters.clientIDs) : + /\ \E clientB \in InitClients({ x.clientID : x \in chainBConnectionEnds }) : chmB!Init("chainB", clientB, NullConnection) /\ InitEnv @@ -330,6 +342,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Tue May 19 09:45:49 CEST 2020 by adi +\* Last modified Tue May 19 11:35:41 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 043c7453ec4eec7fdd183bba8349aac4753dc867 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 19 May 2020 13:16:50 +0200 Subject: [PATCH 51/63] Tried liveness check with buffers of lenght 1, no success --- .../spec/connection-handshake/Environment.tla | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 5ff5cf4057..f3711068c1 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -26,7 +26,7 @@ CONSTANT MaxHeight, \* Maximum height of any chain in the system. ASSUME MaxHeight > 1 -ASSUME MaxBufLen > 1 +ASSUME MaxBufLen >= 1 VARIABLES @@ -224,12 +224,12 @@ MaliciousNextEnv == NextEnv == \/ /\ GoodNextEnv (* A good step. *) /\ UNCHANGED<> -\* \/ /\ ~ maliciousEnv (* Enable malicious env. *) -\* /\ storeChainA.connection.state # "UNINIT" -\* /\ storeChainB.connection.state # "UNINIT" -\* /\ maliciousEnv' = TRUE -\* /\ MaliciousNextEnv -\* /\ UNCHANGED chainStoreVars + \/ /\ ~ maliciousEnv (* Enable malicious env. *) + /\ storeChainA.connection.state # "UNINIT" + /\ storeChainB.connection.state # "UNINIT" + /\ maliciousEnv' = TRUE + /\ MaliciousNextEnv + /\ UNCHANGED chainStoreVars \/ /\ maliciousEnv (* A malicious step. *) /\ MaliciousNextEnv /\ UNCHANGED<> @@ -309,21 +309,15 @@ TypeInvariant == If both chains can progress, we should reach open on both chains. *) -\*Termination == +Termination == \*\* [](chmA!CanAdvance /\ chmB!CanAdvance) \*\* => -\* <> (/\ storeChainA.connection.state = "OPEN" -\* /\ storeChainB.connection.state = "OPEN" -\* /\ bufChainA = <<>> -\* /\ bufChainB = <<>> -\* /\ chmA!CanAdvance -\* /\ chmB!CanAdvance -\* /\ storeChainA.client = storeChainB.client ) - + <> (/\ storeChainA.connection.state = "OPEN" + /\ storeChainB.connection.state = "OPEN") -MockProperty == - <> /\ storeChainA.latestHeight = 3 +\*TerminationPrecondition == +\* <> [](chmA!CanAdvance /\ chmB!CanAdvance) (* Safety property. @@ -342,6 +336,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Tue May 19 11:35:41 CEST 2020 by adi +\* Last modified Tue May 19 13:09:14 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 59f80752f598f9809a47ce7b0cfb25f2cbd9d6c8 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Tue, 19 May 2020 19:13:46 +0200 Subject: [PATCH 52/63] Added a full-fledged termination property; fixed relaying bugs; still left -- remove the turn mechanism --- .../ConnectionHandshakeModule.tla | 17 +- .../spec/connection-handshake/Environment.tla | 202 +++++++----- .../spec/connection-handshake/ICS3Types.tla | 296 ++++++++++++++++++ 3 files changed, 437 insertions(+), 78 deletions(-) create mode 100644 verification/spec/connection-handshake/ICS3Types.tla diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index c096ed8bb5..6e5357e60f 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -136,7 +136,8 @@ GetClientProof == client on this chain stores the height reported in the proof. Note that this condition should eventually become true, assuming a correct environment, which should periodically update the client on each chain; see actions 'GoodNextEnv' - and 'UpdateClient'. + and 'UpdateClient'. + *) VerifyConnProof(cp) == /\ cp.height \in store.client.consensusStates @@ -258,6 +259,7 @@ PreconditionsConfirmMsg(m) == /\ m.connProof.connectionState = "OPEN" /\ VerifyConnProof(m.connProof) + (* Handles a "ICS3MsgConfirm" message. *) HandleConfirmMsg(m) == @@ -291,10 +293,12 @@ CanAdvance == This will also advance the chain height. *) UpdateClient(height) == - /\ height \notin store.client.consensusStates - /\ store' = [store EXCEPT !.latestHeight = @ + 1, - !.client.consensusStates = @ \cup {height}, - !.client.latestHeight = height] + \/ /\ height \notin store.client.consensusStates + /\ store' = [store EXCEPT !.latestHeight = @ + 1, + !.client.consensusStates = @ \cup {height}, + !.client.latestHeight = height] + \/ /\ height \in store.client.consensusStates + /\ UNCHANGED store (* Generic action for handling any type of inbound message. @@ -333,6 +337,7 @@ Next == \/ /\ inBuf = <<>> /\ ~ CanAdvance /\ UNCHANGED<> + \/ UNCHANGED<> TypeInvariant == @@ -344,6 +349,6 @@ TypeInvariant == ============================================================================= \* Modification History -\* Last modified Tue May 19 11:29:24 CEST 2020 by adi +\* Last modified Tue May 19 17:48:10 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index f3711068c1..9941d0c596 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -30,33 +30,36 @@ ASSUME MaxBufLen >= 1 VARIABLES - bufChainA, \* A buffer (sequence) for messages inbound to chain A. - bufChainB, \* A buffer for messages inbound to chain B. + turn, + inBufChainA, \* A buffer (sequence) for messages inbound to chain A. + inBufChainB, \* A buffer for messages inbound to chain B. + outBufChainA, \* A buffer for messages outgoing from chain A. + outBufChainB, \* A buffer for messages outgoing from chain B. storeChainA, \* The local store of chain A. storeChainB, \* The local store of chain B. maliciousEnv \* If TRUE, environment interferes w/ CH protocol. -(************* chainAConnectionEnds & chainBConnectionEnds ***************** +(************* ChainAConnectionEnds & ChainBConnectionEnds ***************** The set of records that each chain can use as a valid local connection end. For each chain, this set contains one record, since we are modeling a single connection in this specification. ***************************************************************************) -chainAConnectionEnds == +ChainAConnectionEnds == [ connectionID : { "connAtoB" }, clientID : { "clientOnAToB" } ] -chainBConnectionEnds == +ChainBConnectionEnds == [ connectionID : { "connBtoA" }, clientID : { "clientOnBToA" } ] AllConnectionEnds == - chainAConnectionEnds \union chainBConnectionEnds + ChainAConnectionEnds \union ChainBConnectionEnds AllClientIDs == { x.clientID : x \in AllConnectionEnds } @@ -66,39 +69,43 @@ AllConnectionIDs == (* Bundle with variables that chain A has access to. *) -chainAVars == <> (* The local chain store. *) (* Bundle with variables that chain B has access to. *) -chainBVars == <> (* Local chain store. *) (* All variables specific to chains. *) chainStoreVars == <> -allVars == <> +allVars == <> INSTANCE ICS3Types chmA == INSTANCE ConnectionHandshakeModule WITH MaxChainHeight <- MaxHeight, - inBuf <- bufChainA, - outBuf <- bufChainB, + inBuf <- inBufChainA, + outBuf <- outBufChainA, store <- storeChainA, - ConnectionIDs <- { x.connectionID : x \in chainAConnectionEnds }, - ClientIDs <- { x.clientID : x \in chainAConnectionEnds } + ConnectionIDs <- { x.connectionID : x \in ChainAConnectionEnds }, + ClientIDs <- { x.clientID : x \in ChainAConnectionEnds } chmB == INSTANCE ConnectionHandshakeModule WITH MaxChainHeight <- MaxHeight, - inBuf <- bufChainB, (* Flip message buffers *) - outBuf <- bufChainA, (* Inbound of "A" is outbound of "B". *) + inBuf <- inBufChainB, + outBuf <- outBufChainB, store <- storeChainB, - ConnectionIDs <- { x.connectionID : x \in chainBConnectionEnds }, - ClientIDs <- { x.clientID : x \in chainBConnectionEnds } + ConnectionIDs <- { x.connectionID : x \in ChainBConnectionEnds }, + ClientIDs <- { x.clientID : x \in ChainBConnectionEnds } (*************************************************************************** @@ -157,31 +164,59 @@ chmB == INSTANCE ConnectionHandshakeModule *) InitEnv == /\ maliciousEnv = FALSE - /\ \/ /\ bufChainA \in {<> : (* ICS3MsgInit to chain A. *) - msg \in InitMsgs(chainAConnectionEnds, chainBConnectionEnds)} - /\ bufChainB = <<>> - \/ /\ bufChainB \in {<> : (* ICS3MsgInit to chain B. *) - msg \in InitMsgs(chainBConnectionEnds, chainAConnectionEnds)} - /\ bufChainB = <<>> - \/ /\ bufChainA \in {<> : (* ICS3MsgInit to both chains. *) - msg \in InitMsgs(chainAConnectionEnds, chainBConnectionEnds)} - /\ bufChainB \in {<> : - msg \in InitMsgs(chainBConnectionEnds, chainAConnectionEnds)} + /\ \/ /\ inBufChainA \in {<> : (* ICS3MsgInit to chain A. *) + msg \in InitMsgs(ChainAConnectionEnds, ChainBConnectionEnds)} + /\ inBufChainB = <<>> + \/ /\ inBufChainB \in {<> : (* ICS3MsgInit to chain B. *) + msg \in InitMsgs(ChainBConnectionEnds, ChainAConnectionEnds)} + /\ inBufChainB = <<>> + \/ /\ inBufChainA \in {<> : (* ICS3MsgInit to both chains. *) + msg \in InitMsgs(ChainAConnectionEnds, ChainBConnectionEnds)} + /\ inBufChainB \in {<> : + msg \in InitMsgs(ChainBConnectionEnds, ChainAConnectionEnds)} + /\ outBufChainA = <<>> + /\ outBufChainB = <<>> + + +Relay(from, to) == + /\ from # <<>> + /\ Len(to) < MaxBufLen - 1 + /\ to' = Append(to, Head(from)) + /\ from' = Tail(from) (* Default next (good) action for Environment. + TODO: should advance the height non-deterministically? + May change either of the store of chain A or B. *) GoodNextEnv == \/ /\ chmA!CanAdvance /\ \/ chmA!AdvanceChainHeight \/ chmA!UpdateClient(storeChainB.latestHeight) - /\ UNCHANGED storeChainB + /\ UNCHANGED<> \/ /\ chmB!CanAdvance /\ \/ chmB!AdvanceChainHeight \/ chmB!UpdateClient(storeChainA.latestHeight) - /\ UNCHANGED storeChainA + /\ UNCHANGED<> + + +RelayNextEnv == + \/ LET msg == Head(outBufChainA) + targetHeight == IF MessageTypeIncludesConnProof(msg.type) + THEN msg.connProof.height + ELSE storeChainA.latestHeight + IN /\ Relay(outBufChainA, inBufChainB) + /\ chmB!UpdateClient(targetHeight) + /\ UNCHANGED<> + \/ LET msg == Head(outBufChainB) + targetHeight == IF MessageTypeIncludesConnProof(msg.type) + THEN msg.connProof.height + ELSE storeChainB.latestHeight + IN /\ Relay(outBufChainB, inBufChainA) + /\ chmA!UpdateClient(targetHeight) + /\ UNCHANGED<> (* Environment malicious behavior. @@ -190,52 +225,61 @@ GoodNextEnv == This interferes with the ICS3 protocol by introducing additional messages that are usually incorrect. - Without the first constraint, on the "Len(bufChainA)" and - "Len(bufChainB)", Env could fill buffers (DoS attack). This can + Without the first constraint, on the "Len(inBufChainA)" and + "Len(inBufChainB)", Env could fill buffers (DoS attack). This can lead to a deadlock, because chains will simply be unable to reply to each other. *) MaliciousNextEnv == - \/ /\ Len(bufChainA) < MaxBufLen - 1 - /\ bufChainA' \in {Append(bufChainA, arbitraryMsg) : - arbitraryMsg \in ConnectionHandshakeMessages} - /\ UNCHANGED bufChainB - \/ /\ Len(bufChainB) < MaxBufLen - 1 - /\ bufChainB' \in {Append(bufChainB, arbitraryMsg) : - arbitraryMsg \in ConnectionHandshakeMessages} - /\ UNCHANGED bufChainA + \/ /\ Len(inBufChainA) < MaxBufLen - 1 + /\ inBufChainA' \in {Append(inBufChainA, arbitraryMsg) : + arbitraryMsg \in {msg \in ConnectionHandshakeMessages : + msg.type = "ICS3MsgTry"}} + /\ UNCHANGED inBufChainB + \/ /\ Len(inBufChainB) < MaxBufLen - 1 + /\ inBufChainB' \in {Append(inBufChainB, arbitraryMsg) : + arbitraryMsg \in {msg \in ConnectionHandshakeMessages : + msg.type = "ICS3MsgTry"}} +\* arbitraryMsg \in ConnectionHandshakeMessages +\* /\ arbitraryMsg.type = "ICS3MsgTry"} + /\ UNCHANGED inBufChainA (* Environment next action. +TODO: explain the relaying functionality and additional variables. + There are four possible actions that the environment may perform: - + 1. A good step: the environment advances or updates the client on one of the two chains. - + 2. The environment becomes malicious, as a result of both chains advancing past the UNINIT step (i.e., after both chains finished locking on to a set of connection parameters). - + 3. A malicious step. - + 4. The environment stops acting maliciously. *) NextEnv == \/ /\ GoodNextEnv (* A good step. *) - /\ UNCHANGED<> + /\ UNCHANGED maliciousEnv + \/ /\ RelayNextEnv + /\ UNCHANGED maliciousEnv + \/ /\ UNCHANGED <> \/ /\ ~ maliciousEnv (* Enable malicious env. *) /\ storeChainA.connection.state # "UNINIT" /\ storeChainB.connection.state # "UNINIT" /\ maliciousEnv' = TRUE /\ MaliciousNextEnv - /\ UNCHANGED chainStoreVars + /\ UNCHANGED<> \/ /\ maliciousEnv (* A malicious step. *) /\ MaliciousNextEnv - /\ UNCHANGED<> + /\ UNCHANGED<> \/ /\ maliciousEnv (* Disable malicious env. *) /\ maliciousEnv' = FALSE - /\ UNCHANGED<> + /\ UNCHANGED<> (* Enables when the connection is open on both chains. @@ -245,7 +289,7 @@ NextEnv == ICS3ReachTermination == /\ storeChainA.connection.state = "OPEN" /\ storeChainB.connection.state = "OPEN" - /\ UNCHANGED <> + /\ UNCHANGED allVars (* Enables when both chains attain maximum height, if the connection is still @@ -258,40 +302,48 @@ ICS3ReachTermination == ICS3NonTermination == /\ (~ chmA!CanAdvance \/ storeChainA.connection.state # "OPEN") /\ (~ chmB!CanAdvance \/ storeChainB.connection.state # "OPEN") - /\ UNCHANGED <> + /\ UNCHANGED allVars (****************************************************************************** - Main spec. - The system comprises the connection handshake module & environment. + + Main spec. The system comprises the environment plus the two instances of + ICS3 modules. + *****************************************************************************) (* Initializes both chains, attributing to each a chainID and a client. *) Init == - /\ \E clientA \in InitClients({ x.clientID : x \in chainAConnectionEnds }) : + /\ turn = "ENV" + /\ \E clientA \in InitClients({ x.clientID : x \in ChainAConnectionEnds }) : chmA!Init("chainA", clientA, NullConnection) - /\ \E clientB \in InitClients({ x.clientID : x \in chainBConnectionEnds }) : + /\ \E clientB \in InitClients({ x.clientID : x \in ChainBConnectionEnds }) : chmB!Init("chainB", clientB, NullConnection) /\ InitEnv (* The two ICS3 modules and the environment alternate their steps. *) Next == - \/ ICS3ReachTermination - \/ ICS3NonTermination - \/ NextEnv - \/ chmA!Next /\ UNCHANGED <> - \/ chmB!Next /\ UNCHANGED <> +\* \/ ICS3ReachTermination +\* \/ ICS3NonTermination + \/ turn = "ENV" /\ NextEnv /\ turn' = "A" + \/ turn = "A" /\ chmA!Next /\ UNCHANGED <> + /\ turn' = "B" + \/ turn = "B" /\ chmB!Next /\ UNCHANGED <> + /\ turn' = "ENV" FairProgress == /\ WF_chainAVars(chmA!Next) /\ WF_chainBVars(chmB!Next) - /\ \A height \in 1..MaxHeight : WF_storeChainA(chmA!UpdateClient(height)) - /\ \A height \in 1..MaxHeight : WF_storeChainB(chmB!UpdateClient(height)) - /\ WF_storeChainA(chmA!AdvanceChainHeight) - /\ WF_storeChainB(chmB!AdvanceChainHeight) + /\ WF_<>(RelayNextEnv) +\* /\ <> ~ maliciousEnv +\* /\ <> ~ activeEnv +\* /\ \A height \in 1..MaxHeight : WF_storeChainA(chmA!UpdateClient(height)) +\* /\ \A height \in 1..MaxHeight : WF_storeChainB(chmB!UpdateClient(height)) +\* /\ WF_storeChainA(chmA!AdvanceChainHeight) +\* /\ WF_storeChainB(chmB!AdvanceChainHeight) Spec == @@ -305,19 +357,25 @@ TypeInvariant == /\ chmB!TypeInvariant +(* Action ProcessMsg will eventually enable & chainAVars + will not be unchanged. *) +MessageLivenessPre == + \E m \in ConnectionHandshakeMessages : m = Head(inBufChainA) + => <><>_chainAVars + + (* Liveness property. If both chains can progress, we should reach open on both chains. *) Termination == -\*\* [](chmA!CanAdvance /\ chmB!CanAdvance) -\*\* => - <> (/\ storeChainA.connection.state = "OPEN" - /\ storeChainB.connection.state = "OPEN") - + []((/\ <> ~ maliciousEnv + /\ storeChainA.latestHeight < MaxHeight - 4 + /\ storeChainB.latestHeight < MaxHeight - 4 + /\ MessageLivenessPre) + => <> (/\ storeChainA.connection.state = "OPEN" + /\ storeChainB.connection.state = "OPEN")) -\*TerminationPrecondition == -\* <> [](chmA!CanAdvance /\ chmB!CanAdvance) (* Safety property. @@ -336,6 +394,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Tue May 19 13:09:14 CEST 2020 by adi +\* Last modified Tue May 19 18:20:06 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/ICS3Types.tla b/verification/spec/connection-handshake/ICS3Types.tla new file mode 100644 index 0000000000..5aed28e75c --- /dev/null +++ b/verification/spec/connection-handshake/ICS3Types.tla @@ -0,0 +1,296 @@ +----------------------------- MODULE ICS3Types ----------------------------- + +(*************************************************************************** + + This module is part of the TLA+ high-level specification for the + IBC Connection Handshake protocol (ICS3). + + This module includes common domain definitions that other modules will + extend. + + + ***************************************************************************) + +EXTENDS Naturals + +CONSTANTS MaxHeight, + AllConnectionIDs, + AllClientIDs + + +(******************************* InitClients ***************************** + A set of records describing the possible initial values for the + clients on a chain. + + A client record contains the following fields: + + - consensusStates -- a set of heights, each height being a Nat + Stores the set of all heights (i.e., consensus states) that this + client observed. At initialization time, the client only observes + the first height, so the only possible value for this record is + {1}. + + - clientID -- a string + The identifier of the client. This is expected as a parameter, since + it is a chain-specific field at initialization time. + + - latestHeight -- a natural number + Stores the latest height among all the heights in consensusStates. + Initialized to 1. + + ***************************************************************************) +InitClients(specificClientIDs) == + [ + consensusStates : {{1}}, + clientID : specificClientIDs, + latestHeight : {1} + ] + + +(***************************** InitMsgs *********************************** + + The set of ConnectionHandshakeMessage records where message type is + ICS3MsgInit. + + This operator returns the set of all initialization messages, such that + the local end is the set 'le', and the remote end is set 're'. + + ***************************************************************************) +InitMsgs(le, re) == + [ + type : {"ICS3MsgInit"}, + parameters : [ + localEnd : le, + remoteEnd : re + ] + ] + + +(******************************* ICS3MessageTypes ***************************** + + The set of valid message types that the ConnectionHandshakeModule can + handle, e.g., as incoming or outgoing messages. + + In the low-level connection handshake protocol, the four messages have + types: ConnOpenInit, ConnOpenTry, ConnOpenAck, ConnOpenConfirm. + In this high-level specification, we choose slightly different names, to + make an explicit distinction to the low-level protocol. Message types + are as follows: + ICS3MsgInit, ICS3MsgTry, ICS3MsgAck, and ICS3MsgConfirm. + For a complete description of the message record, see + ConnectionHandshakeMessage below. + + ***************************************************************************) +ICS3MessageTypes == + { + "ICS3MsgInit", + "ICS3MsgTry", + "ICS3MsgAck", + "ICS3MsgConfirm" + } + + +(******************************* ICS3ConnectionStates ********************** + + The set of valid states that a connection can be in. + + ***************************************************************************) +ICS3ConnectionStates == + { + "UNINIT", + "INIT", + "TRYOPEN", + "OPEN" + } + + +NullClientID == + "NULLClientID" + +NullConnectionID == + "NULLConnectionID" + +(******************************* NullConnectionEnd ************************* + + A special record defining an uninitialized connection end. + + ***************************************************************************) +NullConnectionEnd == + [ + connectionID |-> NullConnectionID, + clientID |-> NullClientID + ] + + +(******************************* NullConnectionParameters ****************** + + A record defining the special null connection parameters. + + ***************************************************************************) +NullConnectionParameters == + [ + localEnd |-> NullConnectionEnd, + remoteEnd |-> NullConnectionEnd + ] + + +(******************************* ConnectionEnds ***************************** + A set of connection end records. + A connection end record contains the following fields: + + - connectionID -- a string + Stores the identifier of this connection, specific to a chain. + + - clientID -- a string + Stores the identifier of the client running on this chain. + + ***************************************************************************) +ConnectionEnds == + [ + connectionID : AllConnectionIDs, + clientID : AllClientIDs + ] + + +(******************************* ConnectionParameters ********************** + A set of connection parameter records. + A connection parameter record contains the following fields: + + - localEnd -- a connection end + Specifies the local connection details (i.e., connection ID and + client ID). + + - remoteEnd -- a connection end + Specifies the local connection details. + + ***************************************************************************) +ConnectionParameters == + [ + localEnd : ConnectionEnds, + remoteEnd : ConnectionEnds + ] + \union + { + NullConnectionParameters + } + + +(******************************* NullConnection **************************** + + Initially, the connection on both chains is uninitialized, defined as + this special record. + + ***************************************************************************) +NullConnection == [ + parameters |-> NullConnectionParameters, + state |-> "UNINIT" +] + + +(******************************* Connections ******************************* + A set of connection records. + A connection record contains the following fields: + + - parameters -- a connection parameters record + Specifies the local plus remote ends. + + - state -- a connection state (see ConnectionStates set). + + ***************************************************************************) +Connections == + [ + parameters : ConnectionParameters, + state : ICS3ConnectionStates + ] + + +(******************************* ConnProof ********************************* + A set of records describing the possible values for connection proofs. + + A connection proof record contains the following fields: + + - connectionState -- a string + Captures the state of the connection in the local store of the module + which created this proof. + + - height -- a Nat + The current height (latestHeight) of the chain at the moment when the + module created this proof. + + ***************************************************************************) +ConnProofs == + [ + connectionState : ICS3ConnectionStates, + height : 1..MaxHeight + ] + + +(******************************* ClientProofs ******************************* + A set of records describing the possible values for client proofs. + + A client proof record contains the following fields: + + - height -- a Nat + The current height (latestHeight) of the client colocated with module + which created this proof. + + ***************************************************************************) +ClientProofs == + [ + height : 1..MaxHeight + ] + + +(******************************* Heights *********************************** + + The set of all possible heights that a chain can assume throughout any + execution. + + ***************************************************************************) +Heights == + 1..MaxHeight + + +(*********************** ConnectionHandshakeMessages *********************** + + The set of ConnectionHandshakeMessage records. + These are connection handshake specific messages that two chains exchange + while executing the ICS3 protocol. + + ***************************************************************************) +ConnectionHandshakeMessages == + [ + type : {"ICS3MsgInit"}, + parameters : ConnectionParameters + ] + \union + [ + type : {"ICS3MsgTry"}, + parameters : ConnectionParameters, + connProof : ConnProofs, + clientProof : ClientProofs + ] + \union + [ + type : {"ICS3MsgAck"}, + parameters : ConnectionParameters, + connProof : ConnProofs, + clientProof : ClientProofs + ] + \union + [ + type : {"ICS3MsgConfirm"}, + parameters : ConnectionParameters, + connProof : ConnProofs + ] + + +MessageTypeIncludesConnProof(type) == + type \in {"ICS3MsgTry", "ICS3MsgAck", "ICS3MsgConfirm"} + + +============================================================================= +\* Modification History +\* Last modified Tue May 19 17:44:07 CEST 2020 by adi +\* Created Mon May 18 17:53:08 CEST 2020 by adi + From 8e73a85e7fa7a33b7041ef5a1f40085cdf612c32 Mon Sep 17 00:00:00 2001 From: Anca Zamfir Date: Tue, 19 May 2020 21:02:27 +0200 Subject: [PATCH 53/63] [WIP] Relayer ADR and diagrams (#28) * relayer ADR and diagrams * some review comments * Relayer ADR updates, move to Rust structs, small config changes * cargo fmt * clean figure * add client heights diagram and explanation, some doc corrections * corrections on event handling diagram, added also consensus state events * small fixes before merge --- docs/architecture/adr-002-ibc-relayer.md | 807 ++++++++++++++++++ .../assets/IBC_client_heights.jpeg | Bin 0 -> 143106 bytes .../assets/IBC_conn_handshake_relay.jpeg | Bin 0 -> 336221 bytes docs/architecture/assets/IBC_relayer.jpeg | Bin 254871 -> 121564 bytes .../assets/IBC_relayer_threads.jpeg | Bin 0 -> 93841 bytes relayer/relay/src/config.rs | 8 +- .../config/fixtures/relayer_conf_example.toml | 29 +- 7 files changed, 830 insertions(+), 14 deletions(-) create mode 100644 docs/architecture/adr-002-ibc-relayer.md create mode 100644 docs/architecture/assets/IBC_client_heights.jpeg create mode 100644 docs/architecture/assets/IBC_conn_handshake_relay.jpeg create mode 100644 docs/architecture/assets/IBC_relayer_threads.jpeg diff --git a/docs/architecture/adr-002-ibc-relayer.md b/docs/architecture/adr-002-ibc-relayer.md new file mode 100644 index 0000000000..f0b6c0ebc5 --- /dev/null +++ b/docs/architecture/adr-002-ibc-relayer.md @@ -0,0 +1,807 @@ +# ADR 002: IBC Relayer in Rust + +## Changelog +* {date}: {changelog} + +## Definitions +These definitions are specific for this document and they may not be consistent with the IBC Specification. + +IBC transaction - a transaction that includes IBC datagrams (including packets). This is constructed by the relayer and sent over the physical network to a chain according to the chain rules. For example, for tendermint chains a `broadcast_tx_commit` request is sent to a tendermint RPC server. + +IBC datagram - is an element of the transaction payload sent by the relayer; it includes client, connection, channel and IBC packet data. Multiple IBC datagrams may be included in an IBC transaction. + +IBC packet - a particular type of IBC datagram that includes the application packet and its commitment proof. + +On-chain IBC Client (or IBC Client) - client code running on chain, typically only the light client verification related functionality. + +Relayer Light Client - full light client functionality, including connecting to at least one provider (full node), storing and verifying headers, etc. + +Source chain - the chain from which the relayer reads data to fill an IBC datagram. + +Destination chain - the chain where the relayer submits transactions that include the IBC datagram. + +A and B chains - for connection protocol, A is the "initiating" chain where `MsgConnectionOpenInit` is initially processed and eventually `MsgConnectionOpenAck`. B is the chain where `MsgConnectionOpenTry` and `MsgConnectionOpenConfirm` are processed. +Similar for channel handshake protocol. + +## Context +A relayer is an off-chain process responsible for relaying IBC datagrams between two or more chains by scanning their states and submitting transactions. This is because in the IBC architecture, modules are not directly sending messages to each other over networking infrastructure, but instead they create and store the data to be retrieved and used by a relayer to build the IBC datagrams. + +This document provides an initial Rust implementation specification of a relayer that interconnects Cosmos-SDK/ Tendermint chains. + +The diagram below shows a high level view of the relayer and its interactions with the source and destination chains. The next sections go in more details of the different interactions. + +![IBC Relayer Architecture Diagram](assets/IBC_relayer.jpeg). + +## Assumptions and Dependencies +This section covers assumptions and dependencies about the chains and their IBC implementation. The first implementation focuses on and will only be tested with Cosmos-SDK/ Tendermint chains. In addition, functionality required by the relayer that is outside the scope of this document, and the availability of their implementations is considered. + +#### Data Availability +The relayer monitors the chain state to determine when packet forwarding is required. The relayer must be able to retrieve the data within some time bound. This is referred to as **data availability**. + +#### Data Legibility +IBC protocol defines the minimal data set that must be made available to relayers for correct operation of the protocol. The relayer expects the data to be legible, i.e. **data should be serialized** according to the IBC specification format; this includes consensus state, client, connection, channel, and packet information, and any auxiliary state structure necessary to construct proofs of inclusion or exclusion of particular key/value pairs in state. + - [IBC Specification] some protobuf specifications can be found under individual ICS-es, for exmple [ICS-03 connection protobufs](https://github.com/cosmos/ics/blob/master/spec/ics-002-client-semantics/data-structures.proto) + Note: Some relayer development is blocked on SDK and Tendermint migration to protobuf encoding. Current work is done in [migration to protobuf](https://github.com/cosmos/cosmos-sdk/pull/6097) + +#### Query Functionality +IBC host state machines MUST expose an interface for inspecting their state. For Cosmos/Tendermint chains this means: +- the IBC modules on chain correctly implement and respond to queries + - [IBC-Modules-Rust] an implementation for some queries currently exist in Cosmos-SDK and same and more need to be implemented in Rust. The full requirements are detailed in section Relayer Queries. +- the relayer needs the ability to send rpc/http ABCI queries to and receive replies from Tendermint/Cosmos-SDK + - [[ABCI Rust](https://github.com/tendermint/rust-abci)] - ABCI Rust implementation + - [IBC-Modules-Rust] identifier validation is required (ICS-024) + - [IBC-Modules-Rust] requires Rust types for all query responses + - [[Merkle-Proofs-Rust](https://github.com/confio/ics23/tree/master/rust)] (candidate implementation) - some query responses include proofs and included in IBC transactions by the relayer (some may be validated, TBD) + +#### IBC Messages +The relayer creates transactions that include IBC messages to manage clients, connections and channels, and send application packets to destination chains. These messages must be defined in the IBC Rust implementation [IBC-Modules-Rust]. + +#### IBC Logging System +IBC packet data & timeouts are not stored directly in the chain state (as this storage is presumed to be expensive) but are instead committed to with a succinct cryptographic commitment (only the commitment is stored). +As a consequence, IBC requires that a **host state machine MUST provide an event logging system** that logs data in the course of transaction execution. **Logs must be queryable** by relayers to read IBC packet data & timeouts. + +The logging system must provide the following functions: + - [IBC-Modules-Go] emitLogEntry for emitting log entries called by the state machine during transaction execution: + - type emitLogEntry = (topic: string , data: []byte) => void + - example: emitLogEntry("sendPacket", {sequence: packet.sequence , data: packet.data, timeout: packet.timeout}) + - [IBC-Modules-Go] queryByTopic for querying past logs matching a given topic: + - type queryByTopic = (height: uint64 , topic: string) => Array < []byte > + +#### Keyring +The relay process must have access to its accounts with tokens on all destination chains, with sufficient balance to pay for transaction fees. Account key information must be stored and managed securely. A keyring implementation is required for CRUD key operations. +[Keyring-Rust] Investigation in existing Rust implementations is needed. (ex: [hwchen-keyring](https://github.com/hwchen/keyring-rs)) + +### Chain Transactions and Signing +The relayer must create chain specific signed transactions. +[Cosmos-Tx-Rust] For the first release Cosmos-SDK transaction signing is required. One possible implementation is [iqlusion's sdtx crate](https://github.com/iqlusioninc/crates/tree/develop/stdtx) + +#### Implementation of IBC "routing module" +The default IBC handler uses a receiver call pattern, where modules must individually call the IBC handler in order to bind to +ports, start handshakes, accept handshakes, send and receive packets, etc. While this provides flexibility for modules, it imposes extra work on the part of the relayer processes that now needs to track the state of multiple modules. The IBC specification describes an IBC “routing module” to route packets, and simplify the task of relayers. This routing module accepts external datagrams and calls into the IBC handler to deal with handshakes and packet relay. The routing module keeps a lookup table of modules, which it can use to look up and call a module when a packet is received, so that external relayers need only ever relay packets to the routing module. +[IBC-Routing-Module-Go] Initial version of the relayer assumes that chains implement the "routing module" + +#### Batching +The relayer may batch IBC datagrams in a single transaction if supported by destination chain and allowed by configuration. In this case the relayer can amortise any overhead costs (e.g. signature checks for fee payment). +Initial version of the relayer assumes batching is supported by all chains. An option may be later included in the configuration file. + +## Relayer Requirements + +A correct relayer MUST: + +- **[R-config-start]** Read, parse, validate a configuration file upon start and configure itself for the specifed chains and paths +- **[R-transport]** Have access to the networking protocols (e.g. TCP/IP, UDP/IP, or QUIC/IP) and physical transport, required to read the state of one blockchain/ machine and submit data to another +- **[R-provider]** Maintain transport connections to at least one full node per chain +- **[R-query]** Query IBC data on source and destination chains +- **[R-light-client]** Run light clients for source chains and +- **[R-IBC-client]** create and update IBC clients on destination chains +- **[R-accounts]** Own accounts on destination chains with sufficient balance to pay for transaction fees +- **[R-transact]** Create, sign and forward IBC datagram transactions +- **[R-relay]** Perform correct relaying of all required messages, according to the IBC sub-protocol constraints +- **[R-restart]** Resume correct functionality after restarts +- **[R-upgrade]** Resume correct functionality after upgrades +- **[R-proofs]** Perform proof verification (as it will be done on the destination chain) and not forward messages where proof verification fails + +The relayer MAY: +- **[R-config-cli]** Provide ways to change configuration at runtime +- **[R-bisection]** Perform bisection to optimize transaction costs and computation on destination chains +- **[R-relay-prio]** Filter or order transactions based on some criteria (e.g. in accordance with the fee payment model) + +## Implementation +The initial implementation will heavily borrow from the Go relayer implementation that uses a "naive" algorithm for relaying messages. The structure of the configuration file is similar with the one in Go (see [Go-Relayer](https://github.com/cosmos/relayer)) + +### Configuration +(WIP) +Upon start the relayer reads a configuration file that includes global and per chain parameters. The file format is .toml +Below is an example of a configuration file. + +```toml +title = "IBC Relayer Config Example" + +[global] +timeout = "10s" +strategy = "naive" + +[[chains]] + id = "chain_A" + rpc_addr = "localhost:26657" + account_prefix = "cosmos" + key_name = "testkey" + store_prefix = "ibc" + client_ids = ["clA1", "clA2"] + gas = 200000 + gas_adjustement = 1.3 + gas_price = "0.025stake" + trusting_period = "336h" + +[[chains]] + id = "chain_B" + rpc_addr = "localhost:26557" + account_prefix = "cosmos" + key_name = "testkey" + store_prefix = "ibc" + client_ids = ["clB1"] + gas = 200000 + gas_adjustement = 1.3 + gas_price = "0.025stake" + trusting_period = "336h" + +[[connections]] + +[connections.src] + client_id = "clB1" + connection_id = "connAtoB" + +[connections.dest] + client_id = "clA1" + connection_id = "connBtoA" + +[[connections.paths]] + src_port = "portA1" + dest_port = "portB1" + direction = "unidirectional" + +[[connections.paths]] + src_port = "portA2" + dest_port = "portB2" + direction = "bidirectional" + +[[connections.paths]] + src_port = "portA3" + dest_port = "portB3" + src_channel = "chan3-on-A" + dest_channel = "chan3-on-B" +``` +The main sections of the configuration file are: +- `global`: +Relaying is done periodically and the frequency is dictated by the `timeout` parameter. The `strategy` parameter configures the relayer to run a particular relaying algorithm. +- `chains`: +Chain level information including account and key name, gas information, trusting period, etc. All source and destination chains must be listed here. +- paths (`connections`, `connections.paths`): +The relayer may be configured to relay between some application ports, over a number of connections and channels, in unidirectional or bidirectional mode. + +### Initialization + +The relayer performs initialization based on the content of the configuration file: +- the file is parsed and semantically validated +- the chains, connections, ports, channels for which relaying is enabled are stored in the Config structure + +```rust +pub struct Config { + pub global: GlobalConfig, + pub chains: Vec, + pub connections: Option>, +} + +pub enum Strategy { + Naive, +} + +pub struct GlobalConfig { + pub timeout: Duration, + pub strategy: Strategy, +} + +pub struct ChainConfig { + pub id: ChainId, + pub rpc_addr: net::Address, + pub account_prefix: String, + pub key_name: String, + pub client_ids: Vec, + pub gas: u64, + pub trusting_period: Duration, +} + +pub struct Connection { + pub src: Option, // use any source + pub dest: Option, // use any destination + pub paths: Option>, // use any port, direction bidirectional +} + +pub struct ConnectionEnd { + pub client_id: String, + pub connection_id: Option, // use all connections to this client +} + +pub enum Direction { + Unidirectional, + Bidirectional, +} + +pub struct RelayPath { + pub src_port: Option, // default from any source port + pub dest_port: Option, // default from any dest port + pub src_channel: Option, // default from any source port + pub dest_channel: Option, // default from any dest port + pub direction: Direction, // default bidirectional +} +``` +All `Option` fields with `None` values mean "any" values. For `direction`, default is bidirectional. +All non-`Option` fields are mandatory and must appear in the configuration file. +If the relayer is started with an invalid configuration file, an error is displayed and the realyer process exits. + +### Relayer Commands + +#### Validate +To validate a configuration file: + +`relayer -c config validate ` + +The command verifies that the specified configuration file parses and it is semantically correct. + +#### Light Client Initialization +To initialize a light client: + +`relayer -c light init -x -h ` + +The command initializes the light client for `` with a trusted height and hash. This should be done for all chains for which relaying is performed. + +#### Start +To start the relayer: + +`relayer -c start` + +The command performs the validation as described above and then starts the relayer. + +#### Query +Most of the queries performed while relaying are also available from the CLI. + +`relayer -c query client state [-h ] [-p ]` + +The command queries the full client state of `` on `` at ``, with or without proof depending on the `` flag. Default `` is latest state and `` is `true`. + +`relayer -c query client consensus [-h ] [-p ]` + +The command queries the consensus state of `` at height `` on `` at ``, with or without proof depending on the `` flag. Default `` is latest state and `` is `true`. + +### Relayer Queries +The relayer queries chain state in order to build the IBC messages. It is expected that each chain type provides implementations of these queries. Initial Rust relayer implementation will be tested with Cosmos-SDK/Tendermint chains, and while some of the IBC-Modules functionality in Rust is not required (e.g. handler functions), a "query" crate should be available for the relayer. +For tendermint, the queries use the `abci.RequestQuery` over rpc/http to retrieve the data. + +The format of the public/ provable state query parameters and responses is chain independent and should also be defined in this crate. + +The following queries are required: + +- `query_store_prefix(chain)` - returns the commitment prefix of the chain (returns chain specific []byte, e.g. `ibc` for tendermint) +- `query_all_client_states(chain)` - returns the IBC light clients instantiated on the chain +- `query_client_consensus_state(chain, clientID, height)` - returns the consensus state proof for a light client at a given height if height > 0, else it returns the latest height +- `query_connections(chain)` - returns all connections created on the chain +- `query_client_connections(chain, clientID)` - returns all connections associated with a light client +- ...more to be added + +### Relayer Concurrency Architecture +The following threads are spawned and execute within the relayer process: +- one Tendermint full light client thread, per configured configured source chain. For example if A->C and B->C paths are enabled then there will be two light client threads, one for A and one for B. These threads download light client headers (block header and commits), verify them and store them as trusted headers in the per chain stores. +- one thread for the main relaying functionality, aka relay thread. +- one thread to relay notifications from source chain and to generate IBC events to the relay thread. + +The figure below shows the interactions for the last two threads. +![IBC relayer threads](assets/IBC_relayer_threads.jpeg) + +On start: +1. Communication (channel ?) between the relay and the notification threads is established. +2. The notification thread registers for IBC events. +3. The relay thread creates the IBC datagrams for A, for all configuration triggered events, for clients `MsgCreateClient`, `MsgUpdateClient` and +4. for connections and channels, i.e. `MsgConnOpenInit` and `MsgChannOpenInit` are sent to chains to initiate connection and channel handshake if required. It then waits for events from the notification thread. +5. The notification thread queries the source chain A at latest height and +6. sends IBC events to the relay thread. Then it waits for notifications from A. +7. For each event related to X (connection, channel or packets), the relay thread queries the client and X state on destination B, and +8. the X state on source chain A. +9. With the information collected in previous steps, the relay thread creates a buffer of messages destined to destination B. +10. When the notification thread receives an IBC notification for X it sends it to the relay thread. +11. Steps 11-14 are the same as 6-9 above. + +Initial version will have a single relay thread for all configured paths. Temporary threads may be created for the source and destination queries required. +Future versions may create multiple relay threads. One possibility is to create one for each destination chain Z, responsible for relaying over *->Z paths. Or have a thread pool, selecting an available thread for relaying to a given destination. The notification thread will route the IBC events to the proper thread. Multiple notification threads, e.g. per source, should also be considered. + +### Relayer Algorithm + +A relayer algorithm is described in [relayer algorithm described in IBC Specifigication](https://github.com/cosmos/ics/blame/master/spec/ics-018-relayer-algorithms/README.md#L47) and [Go relayer implementation ](https://github.com/cosmos/relayer/blob/f3a302df9e6e0c28883f5480199d3190821bcc06/relayer/strategies.go#L49.). + +This section describes some of the details of the realy thread algorithm in the Rust implementation. Inputs are the IBC Events and the events of interest are described in Appendix A. + +At high level, for each event from a source chain, the relayer: +- queries client, connection, channels and/or packet related state on source and destination chains, +- creates new datagrams if needed, +- batches multiple datagrams in single transaction, +- signs and submits these transactions to the destination. + +#### Proofs +The relayer must include proofs in some datagrams as required by the IBC handlers. There are two types of proofs: +- proof of some local state on source chain (A). For example, a proof of correct connection state (`ProofInit`, `ProofTry`, `ProofAck`) is included in some of the connection handshake datagrams. The `ConnOpenTry` message includes the `ProofInit` that is obtained from chain A where the connection should be in `INIT` state and have certain local and counterpary identifiers. The message specific sections below go in more details. +- proof that the chain A's IBC client `clB` is updated with a consensus state and height that have been stored on chain B. +- these proofs are verified on chain B against the consensus state stored by the A client at `proof_height`. + +Notes: +The proof checks require the handlers on B to recreate the state as expected on chain A and to verify the proof against this. For this to work the store prefix of A needs to be added as prefix to the proof path (standardized in ICS 24). There is currently no query endpoint for this in Cosmos-SDK/Tendermint and initial relayer version includes a per chain store prefix in the configuration. +The verification on B requires the presence of a consensus state for client A at same height as `proof_height`. + +#### Light Client Messages +After initialization, relayer light clients are created on the destination chains if not already present. +For a successful A->B relay of IBC packets IBC clients must be instantiated on both source and destination chains, potentially by different relayers. The client creation is permissionless and a relayer may create a client if not already present. +```rust +let msg = MsgCreateClient::new(client_id, header, trusting_period, bonding_period, signer); +``` + +The relayer runs its own light client thread for A that periodically retrieves and verifies headers. The relay thread uses the stored headers to update the A-client on chain B with new headers as required. +```rust +let msg = MsgUpdateClient::new(client_id, header, signer); +``` +It is possible that the relay thread needs a more recent trusted header and in this case it would need a mechanism to signal the client thread to retrieve this header. +Since the relayer must pay for all transactions, including `MsgClientCreate` and `MsgClientUpdate`, there are incentives for optimizations. +For example, light client implementation of Tendermint supports bisection and the relayer may choose to send skipping headers to A-client on B, periodically or when required by new IBC datagrams. + +#### IBC Client Consensus State vs Relayer Light Client States vs Chain states +A number of IBC datagrams contain proofs obtained from chain A at a some height `h`. A proof needs to be verified on B against the commitment root for `h` which, for Tendermint clients, is included in the client consensus state at `h+1`. This is because for Tendermint chains the application Hash after applying all transactions in block `n` is included in block at height `n+1`. + +The relayer therefore needs to ensure that the consensus state at `proof_height+1` exists on chain B. + +One proposal is shown below and described in the rest of this section. +![IBC_client_heights](assets/IBC_client_heights.jpeg) + +The relayer creates a light client on B with `hi` and then updates it as required by processing different IBC events. Let `ha'` be the last consensus state for client on B. +When some IBC event for X (connection, channel or packet) is received, it includes the height, let it be `hx-1` at which the event occured on A. +According to the proposal here, the relayer should: +- get the latest consensus state height of client on B, `ha` +- let `h = max(hx, ha)` +- query for item X at height `h-1` and get a proof `p` at this height +- wait for the block at height `hx` to be received, i.e. `Ev{block, hx}` +- get the minimal set of headers from the light client such that `h` verifies against `ha` +- send zero or more `MsgUpdateClient` datagrams and the `MsgX{X, p, h}` in a transaction to B +- if the transaction is successful or `MsgX..` failed, then "consume" the `Ev{X,..}` + - if `MsgX` fails there is nothing that can be done, another relayer must have submitted first +- else raise again the event at `hA-1` if one not already there +- the effect of this is that a new query is made at `hA-1` and since the consensus state at `hA` exists on B, only `MsgX` needs to be sent out + +#### Connection Messages +The relayer queries the source and destination chains of the relaying paths in order to determine if connection handshake datagrams should be sent to destination chains. + +##### Connection Query +The following structures pertain to connection queries and should be detailed in [IBC-Modules-Rust-ADR]. +The structures are shown here for reference. + +```rust +pub struct Counterparty { + pub client_id: ClientId, + pub connection_id: ConnectionId, + pub prefix: CommitmentRoot, +} + +pub struct ConnectionEnd { + pub state: ConnectionState, + pub Id: ConnectionId, + pub client_id: ClientId, + pub counterparty: Counterparty, + pub versions: Vec +} + +pub enum ConnectionState { + "UNINIT", + "INIT", + "TRYOPEN", + "OPEN", +} + +// ConnectionResponse defines the query response for a connection. +// It includes the proof and the height at which the proof was retrieved. +pub struct ConnectionResponse { + pub connection: ConnectionEnd, + pub proof: Option, + pub proof_path: CommitmentPath, + pub proof_height: Height, +} +``` + +#### Connection Relaying + +The figure below shows the four connection handshake message types that can be created during a relay cycle (see the Relayer box and the four actions). For each message the queries (light grey arrows) and expected states on `A` and `B` are shown. For example, if the connection on A is in `OPEN` state and on B in `TRYOPEN`, the relayer will send a transaction to B including the `ConnOpenConfirm` datagram. Once processed on B, the state of connection changes from `TRYOPEN` to `OPEN`. + +![IBC connection handshake relay](assets/IBC_conn_handshake_relay.jpeg) + +##### MsgConnectionOpenInit +The `MsgConnectionOpenInit` message is used to initialize a connection. This is done when the relay thread starts, after loading the configuration that includes the connection information and before entering its event loop. In this section it is assumed the message is relayed to A. +```rust +pub struct MsgConnectionOpenInit { + pub connection_id: ConnectionId, // connAtoB + pub client_id: ClientId, // clB + pub counterparty: Counterparty, // {ClientID: clA, ConnectionID: connBtoA, Prefix: "B_store"> + pub signer: AccAddress +} +``` +The comments show the values of the fields for the diagram above. + +The relayer creates and forwards this message only if it has been explicitly configured with the connection information (see `connections.src` and `connections.dest`sections of the configuration file). + +In order to create a `MsgConnectionOpenInit` the relayer recreates the `ConnectionEnd` from the configuration, as it will be stored on A. The steps are: +- create the `ConnectionEnd` for the B->A path +```rust + let connection_a = getConfiguredConnection(A, B, ..); +``` +- query connection state on chain A and if it already exist then continue with next event +```rust +let existing_a = ibc_query_connection(chainA, connection_a); +if existing_a.state != "UNINIT" { + continue; +} +``` +- create the message +```rust +let init_msg = MsgConnectionOpenInit { + connection_id: connection_a.connection_id, + client_id: connection_a.client_id, + counterparty: Counterparty { + ClientID: connection_a.counterparty.client_id, + connection_id: connection_a.counterparty.connection_id, + prefix: config(B).store_prefix, + } + Signer: config(A).signer, +} +``` +- send `init_msg` in a transaction to B + +##### MsgConnectionOpenTry +The `MsgConnectionOpenTry` defines the message sent by the relayer to try to open a connection. In this section it is assumed to be relayed to B. + +```rust +pub struct MsgConnectionOpenTry { + pub connection_id: ConnectionId, // connBtoA + pub client_id: ClientId, // clA + pub counterparty: Counterparty, // {ClientID: clB, ConnectionID: connAtoB, Prefix: "A_store"> + pub counterparty_versions: Vec, + pub proof_init: CommitmentProof, // proof that connAtoB connection end is stored on Chain A + pub proof_consensus: CommitmentProof, // proof that on A at proof_height (hA), the B client has + // stored B's consensus state at consensus_height (hB) + pub proof_height: Height, // hA, height of A at which relayer retrieved proof_init + pub consensus_height: Height, // hB + pub signer: AccAddress, +} +``` +The comments show the values of the fields for the diagram above. +Note: +- `proof_height` is the height of chain A when relayer created the `proof_init`, hA in the diagram. +- `consensus_height` is the latest height of chain B that chain A has stored in its client `clB` at the time the relayer queried that client, `hB` in the diagram + +The relayer creates a `MsgConnectionOpenTry` for the A->B relay path when an IBC event notification is received. +The steps are: +- let `connAtoB` be the connection identifier on A,`hx` the height when the event occurred and `clA` the client ID of A on B +- query last client state height on B +```rust +let ha_prime = ibc_query_client_state(chainB, 0).height; +``` +- create `UpdateClientMsg`(s) for `clA` on chain B if required (i.e. if `hx` is higher than latest height of `clA` on B) +```rust + let h = max(hx, ha_prime); + let headers = get_minimal_set(h, ha_prime); + let client_msgs = updateClientMsgs(clA, headers, signer); +``` +- send `client_msgs` to B +- query latest height `ha` of A and wait for `ha > h` (Rust TODO) +- query connection with proof at `h` on chain A and if it is not in proper state then continue with the next event +```rust + let query_response = ibc_query_connection_with_proof(chainA, connAtoB, h); + if query_response.connection.state != "INIT" { + continue; + } + let connection_a = query_response.connection; + let proof_init = query_response.proof; + let proof_height := query_response.proof_height; + assert(proof_height = h); +``` +- query the consensus state stored by client `clB` on A +```rust + let consensus_response = ibc_query_consensus_with_proof(chainA, connection_a.client_id); + let proof_consensus = consensus_response.proof; + let consensus_height = consensus_response.proof_height; +``` +- create the `MsgConnectionOpenTry` message with the information collected above. +```rust +let try_msg = MsgConnectionOpenTry { + connection_id: connBtoA, + client_id: clA, + counterparty: Counterparty{ + client_id: connection_a.client_id, + connection_id: connAtoB, + prefix: config(A).store_prefix, + } + proof_init, + proof_consensus, + proof_height, + consensus_height, + signer: config.B.Signer(), +} +``` +- send `try_msg` to B + +When `MsgConnectionOpenTry` is processed on B, the message handler: +- checks that `consensus_height` is valid (smaller or equal than chain B's current height) and within trusting period, +- client `clA` verifies `proof_consensus` for B's consensus state at `consensus_height` and +- client `clA` verifies `proof_init` for the `ConnectionEnd`object that B expects to be present on A at `proof_height`. +The relayer may also perform these verifications before submitting the transaction. + +##### MsgConnectionOpenAck +(WIP) - needs to be updated with correct query sequence + +`MsgConnectionOpenAck` defines the message sent by the relayer to chain A to acknowledge the change of connection state to `TRYOPEN` on Chain B. + +```rust +pub struct MsgConnectionOpenAck { + pub connection_id: ConnectionId, // connAtoB + pub proof_try: CommitmentProof, // proof that connBtoA on Chain B is in TRYOPEN state + pub proof_consensus: CommitmentProof, // proof that on B at proof_height (hB), the A client has + // stored A's consensus state at consensus_height (hA) + pub proof_height: Height, // hB, height of B at which relayer retrieved proof_try + pub consensus_height: Height, // hA + pub versions: , + pub signer: AccAddress, +} +``` +The comments show the values of the fields for the diagram above. +Note: +- `proof_height` is the height of chain B when relayer created the `proof_try`, hB in the diagram. +- `consensus_height` is the latest height of chain A that chain B has stored in its client `clA` at the time the relayer queried that client, `hA` in the diagram + +The relayer creates a `MsgConnectionOpenAck` for the B->A relay path when an IBC event notification is received or when chain B is scanned. The steps are: +- let `connBtoA` be the connection identifier on B +- query connection with proof on chain B and if it is not in proper state then continue with next event +```rust + let query_response = ibc_query_connection_with_proof(chainB, connBtoA); + if query_response.connection.state != "TRYOPEN" { + continue; + } + let connection_b = query_response.connection; + let proof_try = query_response.proof; + let proof_height := query_response.proof_height; +``` +- query connection on chain A and validate its state: +```rust + let connAtoB = connection_b.counterparty.connection_id; + let connection_a = ibc_query_connection(chainA, connAtoB); + if connection_a.state != "INIT" && connection_a.state != "TRYOPEN" { + continue; + } +``` +- create `UpdateClientMsg` for `clB` on chain A if required (i.e. if `proof_height` is higher than latest height of `clB` on A) +```rust + let client_msg = MsgUpdateClient::new(connection_a.client_id, header, signer); +``` +- query the consensus state stored by client `clA` on B: +```rust + let consensus_response = ibc_query_consensus_with_proof(chainB, connection_b.client_id); + let proof_consensus = consensus_response.proof; + let consensus_height = consensus_response.proof_height; +``` +- create the `MsgConnectionOpenAck` message with the information collected above +```rust +let ack_msg = MsgConnectionOpenAck { + connection_id: connAtoB, + proof_try, + proof_consensus, + proof_height, + consensus_height, + signer: config.A.Signer(), +} +``` +- send `client_msg` and `ack_msg` in a transaction to A + +##### MsgConnectionOpenConfirm +(WIP) - needs to be updated with correct query sequence + +`MsgConnectionOpenConfirm` defines the message sent by the relayer to chain B to confirm the opening of a connection on chain A. + +```rust +pub struct MsgConnectionOpenConfirm { + pub connection_id: ConnectionId, // connBtoA + pub proof_confirm: CommitmentProof,// proof that connAtoB on chain A is in OPEN state + pub proof_height: Height, // hA, height of A at which relayer retrieved the proof_confirm + pub signer: AccAddress, +} +``` + +The relayer creates a `MsgConnectionOpenConfirm` for the A->B relay path when an IBC event notification is received or when chain A is scanned. The steps are: +- let `connAtoB` be the connection identifier on A +- query connection with proof on chain A and if it is not in proper state then continue with next event +```rust + let query_response = ibc_query_connection_with_proof(chainA, connAtoB); + if query_response.connection.state != "OPEN" { + continue; + } + let connection_a = query_response.connection; + let proof_confirm = query_response.proof; + let proof_height = query_response.proof_height; +``` +- query connection on chain B and validate its state: +```rust + let connBtoA = connection_a.counterparty.connection_id; + let connection_b = ibc_query_connection(chainB, connBtoA); + if connection_b.state != "INIT" && connection_b.state != "TRYOPEN" { + continue; + } +``` +- create `UpdateClientMsg` for `clA` on chain B if required (i.e. if `proof_height` is higher than latest height of `clA` on B) +```rust + let client_msg = MsgUpdateClient::new(connection_b.client_id, header, config.B.Signer()); +``` +- create the `MsgConnectionOpenConfirm` message with the information collected above +```rust +let confirm_msg = MsgConnectionOpenAck { + connection_id: connBtoA, + proof_confirm, + proof_height, + signer: config.B.Signer(), +} +``` +- send `client_msg` and `confirm_msg` in a transaction to A + +#### Channels +(WIP) +The channel handshake messages are relayed in a similar way as the connection ones. In addition, checks on the state of the underlying connection is performed. + +#### Packet, Timeouts and Acknowledgments +(WIP) +Application packets are not stored in the chain state, only a cryptographic commitment is stored. +The relayer has to query the chain's logging system to get the packet data for a given source port and channel. +The result of the query includes among others: + - the source port and channel identifiers + - the sequence number +These are used to create the packet's commitment path which is then used in a state query to get the packet commitment. + +## Inter-relayer Coordination +Multiple relayers may run in parallel and, while it is expected that they relay over disjoint paths, it could be the case that they may submit same transactions to a chain. In this case only the first transaction succeeds while subsequent fail causing loss of fees. Ideally some coordination would be in place to avoid this but this is out of scope of this document. + +## Relayer Restarts and Upgrades + +## Decision + +> This section explains all of the details of the proposed solution, including implementation details. +It should also describe affects / corollary items that may need to be changed as a part of this. +If the proposed change will be large, please also indicate a way to do the change to maximize ease of review. +(e.g. the optimal split of things to do between separate PR's) + +## Status + +> A decision may be "proposed" if it hasn't been agreed upon yet, or "accepted" once it is agreed upon. If a later ADR changes or reverses a decision, it may be marked as "deprecated" or "superseded" with a reference to its replacement. + +{Deprecated|Proposed|Accepted} + +## Consequences + +> This section describes the consequences, after applying the decision. All consequences should be summarized here, not just the "positive" ones. + +### Positive + +### Negative + +### Neutral + +### Appendix A +The IBC Events, input to the relay thread are described here. + +``` +{"create_client": { + "client_id": , + "client_type": , + } +} + +{"update_client": { + "client_id": , + "client_type": , + } +} + +{"connection_open_init": { + "connection_id": , + "client_id": , + "counterparty_connection_id": , + "counterparty_client_id": , + } +} + +{}"connection_open_try": { + "connection_id": , + "client_id": , + "counterparty_connection_id": , + "counterparty_client_id": , + } +} + +{"connection_open_ack": { + "connection_id": , + } +} + +{"connection_open_confirm": { + "connection_id": , + } +} + +{"channel_open_init": { + "port_id": , + "channel_id": , + "counterparty_port_id": , + "counterparty_channel_id": , + "connection_id": , + } +} + +{"channel_open_try": { + "port_id": , + "channel_id": , + "counterparty_port_id": , + "counterparty_channel_id": , + "connection_id": , + } +} + +{"channel_open_ack": { + "port_id": , + "channel_id": , + } +} + +{"channel_open_confirm": { + "port_id": , + "channel_id": , + } +} + +{"channel_close_init": { + "port_id": , + "channel_id": , + } +} + +{"channel_close_confirm": { + "port_id": , + "channel_id": , + } +} + +{"send_packet": { + "packet_data": String, + "packet_timeout_height": String, + "packet_timeout_timestamp": String, + "packet_sequence": String, + "packet_src_port": , + "packet_src_channel": , + "packet_dst_port": , + "packet_dst_channel": , +} + +{"recv_packet": { + "packet_data": String, + "packet_ack": String, + "packet_timeout_height": String, + "packet_timeout_timestamp": String, + "packet_sequence": String, + "packet_src_port": , + "packet_src_channel": , + "packet_dst_port": , + "packet_dst_channel": , +} +``` + +## References + +> Are there any relevant PR comments, issues that led up to this, or articles referrenced for why we made the given design choice? If so link them here! + +* {reference link} diff --git a/docs/architecture/assets/IBC_client_heights.jpeg b/docs/architecture/assets/IBC_client_heights.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b24e4d997d5c45d21bbeaaad03f8ebb2466e3edd GIT binary patch literal 143106 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 DIx4Og literal 0 HcmV?d00001 diff --git a/docs/architecture/assets/IBC_conn_handshake_relay.jpeg b/docs/architecture/assets/IBC_conn_handshake_relay.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8c6964c4c546e04993abae014c8c0094a406e42f GIT binary patch literal 336221 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

#{P@0d%&rI;&F1p~6>mwW~2#y-fX`GFs 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 z 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

W$aqGA|KtOK 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%0DJu9`tP{t;*aw znNe4gBIZ|(tm9QrDyxx=oGc=Ss5MMpOwj=d41x3QWU+P7z9{IV<35>^^#c@d~VcuP>z{6jUx5tVg+&c3cR6h5@N`_>AYMp zBp{ZGpkY`_Cr{m;(l=9s+SH|dl{#v(?i;%f8cD!2FGSUR*m;q-F3%w^%RJyY*TvX2 z2+attYeyKr>$Gd)(Pmg3a8Gmwp02^&;+GDIy>u@!T1mH$qAVbQ``POg7t_&^T%`)eSC0l#>TS3sYEtrTJ(>)A;X@SG7;0vjlH}`>GxAz|&QDz>*hxFNXvC4xJLN-3 zgR!IS`E4Ci(6Jtt0z=jHTFVTxQR7_~d%%t4;b`IFfsgUpI$|WkryI~4l;Sch zA2-yncuhSpR`M!T8&2Rpo&y(&FY9I$b($x?-{{okVXEcHaQh<=Z~Dr&I9miH&-z0^ z&SVmedms(oosrE`;d9vjeAzrF%}t2t8J46RbT`iAhad!ZZI8^I9blf z6pwaOgo%dd(v;^~0AcwyS8$B}GOJQ{?7h{mY&}gqS__>dwW4)>O&xs}J=3Ew0bHHE zVp0JH$jjtQI>i;pr*>m$MMKfmqm^PR2gj&*ts?GpCJA-(XuJU5g6>im1nynwqX>YF zrd3f9y>}LXC);IjSERY38NJKMJV!fADny&r-B)r9IhO~0(buQjJnRjTJdcnr)S0CZ=>E!2>ffe@D>hmSTs1FNOl?CibC-RBr z6m)3Y+f5EbB;0E<(NBE>)wUH*GhI+|R}Ipo_S`99>Gbte+7>F8!H~|ROR9-Ij+eEt zu=xUrYc0^T#L{0t1rqecRDew3LYI!Zfq?^-xR{6Uz1-(0!b$0HLD^3|rZFzn_>yTp zp2Y4epc5VI{xU9T8c0P?#Iq-+#3FzW{Ka`$#07;X*HryXtp2s@q4ei@8NF7pUxFTcrsHP0ysLcIZxo%UJUg>ayqtAX0 z5dy;i%66g|_ZrtuX6hkn01JtHNU`DJq+aU4kTC_h-0F?nd%FJK;-vO`2bK`B6V_oH zh|em7zg&yb-gOXBt!eA$ml>`PI398Z5=4G=;KYiivMqM9c=D;mzn9A>)eECuT#QZ< zISLsGWp$J?^gr+{>LMsdx5Qhe?b2M;?5sRlUAS}_VuK~;m%p|!ZqhqPhZ!sR^w~Nt zR6=eIsJb~h+38mCz=VLt&vciyVx1Ifr67(u&iWk>l+u^dS6U~pUJYfoUFr2dnU-*! zD4(V^zN8c;vB3Zb(EI02EX4(iNMJ;3SbusV&9C8YTDMD&1j+Ed!>BxcGoUb@!IfGV z{mEhg7%OIHe(_iys*mjGYs_pi)vo^~Yd~Qc7r5$H^>|``co2A+90i$c73Mg>=&AAF z?hv(#2}NVtfqmgKBJ9lW4K~TCf8B!rS0N&Q^?LI8pHUN(KTs1wDtfz_!~>u4F2Aef z+KO`P&8uY3hkilXEG(;i{o76T2M5mFb3gy2HxNWD$ZQwbKLU_}*BQ_EQQi76a7UbD zDvWuj_D3aY>z`Th3H6vQ;FBop!1K_mz}%%o(js4YZXB2@;P{MPZrbE*3xCVF7$y$3 z7Nu|KK?;jE4u?re-MG_df2q>4(6SbA#;2{C?T1%?tNNicBo|Uaxi6&8sQH(u=AxLz zr`q*j*~U)9MWNai`dv-&8)Wq*JSjmqgQqr6S{Jti5BAoC`) zdzpp_MY`3OJC>C{iJ#wfu3C==iWs6D$cERHi3h1F4^OclOlH|E&g zrQfD4;}`Onfh-&v0l;sFT)vo+ke^#G&^(Gqu4uOk;KXJ4)PB3^JgQ#^&?xO4LXP{{ z3xPL#Xwnzz2NA$iLTEEZSAf}mk0DjlC&?905WT$S}_;MHSD=* z3>fLGSp@>o@=5HXA!71w?Y0xmupQtK#WSPQp^Mv?wC`{~T8}H78hxRn|7@rzQC%u~ z!8*-WZxEy#5k*-ZnX(MdzQU2`jQQ=Qqhyjxsz#t@%l^dd%~0|X9-aK6{-b9}lR%Ll zxB7&&5vsz4hDfuo?t$7dlO_fTDY3 z%D8w&ice0j2|ZCE!8RxQq?i9x{VHTg=VN4HpM1Rb;qt?A^P~Etjd9T!r*;dE@M(T8 zL%>nA?VXXz4xTC2jKgc@FoTT#)A?;-dUuZhU)Udc^^Go?>XqU+#_&vE2 zWdo{mzn}#|R^zwLO7F}KZi(BWR3}lo*(nCS6TNdV@9k~x52`l-bIuuhT4&T2>`4Kx zx>e2zTx&80h1apb#^3(-Zu|Y`aL8KH=1%ulwywGvtgqmow~qar$!?)}^#`3-(<8F1b9yl){T5u39_EV|PWs+%62vkBNJ(U>DU6+X#Q3G^`9GJ&O8+l~cbzLdCtUX| zm_}4HFTf~o`sc7+y!)3k`(yLa8Y%W9@TyHJtnFHri25>21y#*>xDof87_x8F#+oT( z^2LHXVO;EI*U~QdWkDzBw8QtoFhPiKQRK@bGo}dDKJ@twHm#d~`1_CR{~sVBlCskF z<`2U5SI0XJ&DpaC2!g+H*st;a`X8^x-}VR=D!K#?r!6fgBUNgaSbFvyp6PxZ%}!ex z#41+>?8eizD>6?bZ+DZNqCroEFa~cq^JCQ@5?1f@04+TBT*_ifZ+>`aqOGl`4J=T1 zl(~82)xRm|kKKow_Sx$!hR%H>1Tve%1-_%>w% z6_jTt!A43ap6;BI5sg1_bb_akzze#!E489lGQ~6q!se;@GSWDFxL5+pgIwfaHo76Ac>BmW|8W zV8n<|4dK~#OyE^NnEn}|#1;m+LVG{);P!Mb_K22*xBu94KzG!rb%G)EQBjSBw;FPZ z(;0@kFEf*c=9-(b6s!=yEhEXRbJ9u6H{E-sSOo5^{&DPbF?DltMSkoMcrAa<%B9a@ z^rQ^U)&xFZ7kdcLItaioiHt%m$&h@HJ&ez*kbM?WfxV|O_48h<$uNJ-+MDg4&&`+x z4UQa=T;K3G81wt(%gg8MVww+p*4vJaJk?Cm|KjMu*4h2`i{JE<<=a=^=j;~`cS>GG zoj=p`QgF+YgSVvEUSWFHVvZ&&am` zBaBC(pM)sB7UXJY3F!;yXxmMSY~*&oL~pK^(|il8?}eee%&zo2v@kpoV^45NdVD8O z&=bpN?+_qj+6#F(T*l}r$r`@?)==^VctMw?cb}?SsyhV8AohGIlT!(0R!&C^%Y+Ry z!s{;Jh!1m%1ltRAK~~;AS=3=WluA4Zye%GD9n)5)!C6n#{Q)okb`=GBb7*MB(|03oRtg12`}KeXyW#9t0cReicgXYdke(Xtr~*ed z9V1NciMpP@^Vv50Sux;KBRxhH#)d|mZ<3LLPiPx|EY=ODZ4NZ07~P1nt0*+{?-xEO ze^qO~;SETGSy4_yK>C1wyNs~8G|9n_rBMf93LSn>EGMtv-P0EZP*!v+ZvMxE~ER z&UmJJme|diO4$B|+6(DCT@HVi`>Z${BIMKS_O>KlBP3n5H?`WFx!vYf{v|^Q0xl8e z=Zk862qJ;9K$t_t`sP4{J{N*-wHMUd>xF8VLR&@jgr^(Zv^_gGUiNsN>?^eGlHaYz zFKPTfv=^i1Vz2ZOlH0r3>1VRM)6jeOAHyiN1CY z->gz$D5;SP%}S`v+p?IU9qmyjM7YGW-wV6uOxkcKr z5YJ+yhCjYXj1?m}x%gEMP+P^7cuT?~KDG>OlqXW07Uf-iLemFKhus6BhXRVg+AtMN z|A{y3N^ZA{Jf;$#)5FsWv9wWzeW2NlPq$$xmI{GDclv!iGLHZ1+`Z=gONEPz<)&&Y z)7&4w3jj!Y*Q*Gjm1fU?hXyUxlovp{DY>;_yYF$*Qg=vW%l9l4l0$FVC)a&G9T6(HFkRGr2%*N6aLy6oqj(E5g);08O`g5Z?T$pJ3*9H_D1hN96GntPR!W6J`)LiIqoLZFGeQpN#qR}Y*;W2+5_UXF$LcsCn$WE{YVL+7Qd?9ZW*@uyew(_V@3Ta9YupnCT{-RflbD}yPVDqi z``s^IwIvx}?!K{*?sbl|bLLk0Soc`Q;E=^MTs4nS-rY4Uo)}9Y72ZK|aa{zU)1KRf ziwo=o9P5jZH8!B6SJ^IY#4>y^hI*dR2Q$W(P@ma-mo25IP#Hgd&L+vECbs*h5}alz zDhR#@+JiwGh|lBQU5<<@~B?c-fAZ*I}3yIKMIk+PMB7zDxeTL(dt=>qdu3wD*K#h$eoSnYj--kd$aWb&m#J z9L_;!BCqRCtQWT2@kPgv9)Iz1_ObNlqQf^Ll=wM;PgdpMm{1I?DTEgRvKxoGPLKY= z%76SH2$>6$2RCx|OnS^7lx>7~E&jxock`P=7F+*r9_+FE5?V$y|R7}xfdt#n89*zZP%%MaBWpRqLgyGyp;jeLGnYI&0vwq+u7(D~sN|L-($D^GbL_$4fb{?7Dzh%$zc@c?@Rsw` z|F%>4AAj`^vE&$Ulpu!#yp*bf+VIifl>AxI$+z9CfCGjOt0i#Py!D86f0@D^V*P{S zOS>U@?Y*G8Z%*g8O1BO3nu5frzPOcZH^mZ*FR>l^iS_*b=a>I`@Oe$8`GTKYw_X|) z-N~&!SK^~Gj_c8A+;3bQg?Z2c@4UVaFLV&FWmP#MN*6f&0k+?-K;ay7dwpS~U?%gg>kKO_cAC$2t z!tu)wFQCQjmwsit@JI3gLZU0LWTs|>y3W^S+o}T0Z0b4 z)6ku>&D5|w_JL5eqgP7(G&!OtC*4%;k~s&*X*<&bqgj-P+^^pXQ?b8-uV$d8gXwez zbBH8*SyEwdn8T)ZfQtpOa`tUrmY|Q_UUO<86ey&UiFdX1i@nIt1X8opF-(f0DTh7G zUP-owBfFNc@<-MGGwUBxTCju~Yk(EyRV5U?QC>9_60PAJHIrOol-y>@7RwkSOx#DfE52{F3|tUJpcFF zlK+!u@!i*Ihs&iR|Cj=#Jp^_<)Kt2vq?QRUr>Ss^Ot-4~5H=T~vuM!%wVe~8ZTtvi zkILps`G`Ao7=*Tf@flApNWckLyElMfS=>|<~;Q4e#cPwE8f5;Y(%#=yr=$!UhA&Me(K zcx3SX&!1V5D0|so*-}{Ftg0B<%1yB;-ZNXG@>e)khQ>3$s6201`pPzab+7X)+b{o= zH|T9(_?eI|oR?o5d+QQBbYe=?Kym2K_Cf#X-U*9)u)|E<iFQbxPOG_GHw#XGhGc-{4sW+paSlU5XR>BwCT`SCFH3wbT z&d>kX0yqToexH<68LgW2Na{XDaxLj+XguZ(a1T$QtJvC=J8x4`>Ser`a~TqSUN^tr z-v5cjk&M5yGd?>Un}%;R+lfDx%{(~gIdH(c#-_1kN zUs)Lrbx=W@(4r&b1FB-*lM+Gw)>$m(th%CADW>D57QT+9M(rlIlyOw#PecJXqf0tr zC7I3#_H%5dELZc4+o^Sb%nCaN25A8 zXHYIct)PNHjm(^ZC{lW!X=f9hye;-&2fPypa^Y7Ky!w#9(mNh#8|v-!<7;oO5f^hA>gr}dW)x#%>X0!Br65~!<61+Oc? zs6K5ZATI1XiNC93A7~8$s@Hy=e-58Wt9MkNOzuUtTi8OSdO`qUZBaz|<207y_Hlgp zBxzLodFrNX11q-Qra5}RX9!@5*~x9d)fO3dtJ~UPFVg9W7Bag-pN!|{F7tH*7T$p$ zI6u?YY1}k^rsZEk)%gO+WCUa*DVvls2MNtmTx9$xI=J#4$tgp7OLmRgyidrk6!6Z3 zi-7fmh>0hgNG7t}+FSg{o5!F3;d=bt-948L`G+f~<4%IbE3LDfbsCoS6LAz}-B zg$V^8^7hJBakFo+xPvCZ5=(==0Au))Qz*751Y(KSVN2NR9xXnpX_37O=IuzH#W~OSWSz^RxOKxiV*p z2OSF07xm$xF}_7i>Gl7N@9QkU@bo%CGE%HQkUS(4>eB^Cu9qcqO0l>#Z^VE7^%m7!&MHFPXnz!0dUL1-56pdWCjAg0h zSmj7J(^|#kFWCd(V-CejT?M!w~6?Y^<2T!9jcSy5M|Bpi^fjmu**j-nZK zU$Pg`d_(K*4@ANyzPq<6RZg6JfAC?AQN<_qhgt$8NG`U<4DuqEZJzx#*MT!6o% z_xl`Ik^AcRUZB>i)l?8IZ!lpN*9MdFEF>;pRV0)9^(d5*+#2~0^*b#MlaekA3QRwG z*zAe(htB-^$D8J4NX_&Qqc~A`Nkxg9JOzqP$cw}Ui?!_~e%@8}xJX_28UJX#7sz+L zSnjhB3Q%B7fs8!+al1|TWtn>STM(cXTF50j*#=r2$qmOIHA{2t+8s}Do~&B1c)~1C z64$r{2~Tm?Q@ZFYteH67c@FFi)NB!sq_0+_mcBDrV=0No2&2eeAN}L4fB1=yJ~NM| zwzBy6bm7lMj?N;)w-exvZfnm`39bI)6&If-{A>b_a3!I(w{bX;m-~nL9)v60cXhec zTMr;T)%xiw_WpDxcqqB3W-i6XR)&l+{K3zXA?$ji#yPwnki>&&Lo?(G(JBLUTH?0# zazp#LS>SeklzRsDA$jS66d%{id|Cj300^ z+WIqR^?Vh+#z}&9+wiLQK#6{-wDQLyBf$JYM2wOMmrxnEcyzTThw*}w+K(IkXdpV7 zHBxReMa^5dI`$#&E$_>-xIZraKi2v^tdK`dl}q-NQCG&Ix;j^&lu>WdKyLWu)a~0# zWShB&L>66S5()Eb!at1up87rntZ)2%Tp0fa?zH{(>P>#O_P%eqo>qQkdm*ODu@Rs4 zk08o6lRJ*aC2S|lzU4UU-I__JJQO~7({21cX|Ywcf9}iXesGw(izb<0YH#+7XVU#S zG1WiCQ6BM5#L(iun5@g(JevH?)BlX+M?;wc>+QuEt;>IKPo!?kv))Xv*an;beGQ8i zj1|VCR(Ttvb0&d1kS z+o0F4DPP$PhaI2Hhh>JQZlW=DTQfI4b~%yEb##s@IFD%Y-W&fx_)K=ghv#?%BQ&%@ zo$V-=bJo%b$ua@vD@)@{eeZ`X|p@?R~W-+kFaMQ%jqfB4@i_X5VVz0rE)DM zR?=;v!TeHa^4v&DQvj{!UIAE6aNf0dRb5%Vm;h>Oi0B_KL!d+sG8cKEA~~u>^PjlN z2{|*K>YVBldE$~62c2={8U;lu_lNb!HV~fV5yrxf;WchifVcNXZ%1*hLMpVeXcppQ z3gOTo)@sj1nYt7Yi4Y4DmPomP=FK(WDBwvi@! z?9~KKxW&Q#!Tq0?&zs<-``Va7E(7%9=$L|$G=tt@cj(4g#@^g;N>hHZ|Pd z{8C=EXn~H2iX*A)oqx}HQ~jqfx~yGrF$QqGZo4}sU0Zn95@_+>JJLujJqKJuDaYg@ zD5tyC-&>z;U0KcQwc7;+9QQamNX`yXb2SFN&WYh+OuR!8KyTtZqfENk_oc8uNL>oG z?ivp&U~1~#Jfz2p;wP;=(@wxZon@1Ln`s@F=KcV=(E~7q;d8B~my_Q`1#>}2D*_iV zS(mD8XDM5ydzF>}*VOddJsmQl#B7IOr2A|dAFPe{e5kSOUE6*(6;0cHr@`V-2f<3* zgIy}tK+}Ll;mVe?;uE@hSkVk+Gr>F{d@nX{&E3?*#3#>NySst$h@ER$TL+P|C`{S! zXahOS*kQ4_3Re|Mm}zVKmo?HG+3pYGshTEqB+qU*CdEFg_+5F#1)MZ0tS8)R?z3_U zs5w@DkW0Bf+dfA2VBu%K24kyWVqqa6q_t00W6pw;QpT6u50=wwn2zau#2m>@LAtGG zhkgM#Ck>UP(V*|uEUkDa%d1{&rwHH;)rxS34?A&sg8Maq}O1XBn8(V z(L#WsxuNjJ5Tc+QNASp_@rCtQ2ea_FLR@iOZ#|<+6=x|?GjcR~l6Z8}*-HSbF)0K& z9H`)~XL!nL7N9ig;&^#YOfdha*rd1x9=||K%Qk*&nH%TB*w2l4d3pBk7jcWO^x}D! z#5%=IeRi>u8WbtrW3uK=E6`fTIymje*@Xypi{H`nJCmN}PC` zY;(SN9+_mVaC$?{S}$5m3`Mh$C|wa32{m%KxG@w5H0+UxT#wAkszvhK`Nr}h^nU3s zMv;?sT+md=LvoRs+j`m-hH2u|d!~G35?da%th=RU3ll`YY2Nb~ue)*lzzF$)SX6{h znQI~FO7JjStJTa+&K_L3f*S>=8YeVH1J%a*qLp= zD`gEBkrur>>dFJBH1j=a!M&7i{|2bKx?t)S)o*a9vX#i$sRhDgFlws-uj&_~?DS6Y zm(2eZ6-qNGLf!B82I&6EQ0Sqy0fh4FX}nS8ZbL@ixR0P<(+4O!SHpg@?w(TF6dU@M z8ooM-mk9v_O3kVU8s$DJNb4Hy-HO+nJVoP6ZY2wPc6rhH`xbQ!@3ek))W)NENCE;> zF8(*ZmvUX^3&91XoTT(;5L5mV5>0s-F=OITUL@YI{vaLqR_aX>tCtgr{D0Vc@3^M2 zb$vYQpkqNrM3iEoN)Qk*AOXZe?;#1n(3C2nNymVoC@4W-=pCd65{i`2iy~4&lM+g3 zA_NG%ckml$)H}{ObIzQ5Kj+N--QWC^?6vn=@4NQ9_gZ_E_kA9cdR=XI1(YGJ9a!Qp zsPU}E$su;aQH})1WK^FK-mn%j2>!4_7{DvGoW=`+pQ#z>5D0GL3L49X0h(te;!XV`d_csJA15 zSDqXe)pT=jo%(S} z-p!x9@=`g*WZLV@+MGQ{*`aj53Mrl}6Bjk<^bE^cL4~GqF3hT+bH|gAWDh9{Hi4I0 zz*@RAt$TB zOD+VmHMpz^yI3S_%_TCcK#<*59bkndM`OOg1J@bFW3oU2&9@!#a78{N$Lu<$Y8%-( z+^qU$?33UDKB0?XpL{)($@Fv1gp>0XlD=GO+sqrZbpT#E11Y9cZNw(RRD)AToc9Yk z$BQeLMI&+M3eVWNLxLtgm~)G9VVyg)CDBP#CBDrS}I@UERhgZyRC|O z5mN$}qlGH@c~_^2rwNDPBTFJTW?k-^o863D>u$v zrTB|?$`4HX1yVRnk>xJNcy^Nw2M7(@fQCK-xjrTpjvAjFxNtH|#Jb;ty$Oa}Kqt8f zEnurL`T^|qTBZB}83BX70wUnE)&|8y8gz+4{6!}gVob0pw}R}@GUGwY?s>IF$dQ9l zCYJ8}{Ks)u!S_q^)f zb9sWawQ<{!^?>@aoyV0LAAfj#VT)sKb^nIU^M6aOlV_E!5v(5h-6T->Chi_^mj0ns z{>?Elv!#%19bBaS-G&NqM`r%l27+R#xE{ozpiHt=uur&n>XG)AC}byns6ivt*(`Yr zQWZ=)dHsLiNfD8?DYJ*04?+W{o8?xMJ-M$zwJXz>*O6gYW-OhHfS{|xZWQY?z82W4 z<;n=xL0S(m^0`Bx^#FmNdjMr?N83%F8}wh23MK~EKKBN!rmVZZKizWvKC^s>XWJ4j z?ClGp?DZwk3iFS=@kgvA(s7af&r5i{F-}ncI^^eh)B*5kVx4kE&pQ*?Z--f1`X0}A zHg~IPiU1?W7mTrSVJCAn zPRxkNbCN6WWrT``W@`vZCzwp1ITuA`Huq#Q+6-H;)i~i4p~WLC7{{(y8LKJ{INk}^ z!+bxNJnKi$l{?ZZTP>|SV2tOh+7A=}nH4pKKy^Quq?ag6Qy^}{@`H9S7I;Nk95dp2 zUSOhB89KF9OiN9pF__a_+SB-C8z!jz$cLM<%id|ZqEU~!GE=-tqdHfF#dHk)(#)~Fh3R%G-%#HDUOWvBgT(EfQ|T^7Bx4)zRZAf3{+)|a}na`Jw|$%FZf zZ2ifMr>k9q=<7dP2^WfiSZb3%>2W{C9w)jgFX;F--4w)XAo)?}AW&OuxcP|1tQq`% zBv+dl^uYV`m@Z!7W7lbv#&m`TUEG2pI9Zs2T0W!OhW=*|_?%>mIJMM!LDXl`r{v}( zh#B*D9(R#6Dp6obI?lWo&L*(I=ai8R0pfaR`CG>GURRCP!sRMB!}Em_M%SbR+!7dZG0U+}ohzOjHJ zAA7Dp6fY5P@*y4!C&RM%7<5M4%KApiba@lQW{u5kn3_Mv$uMMKuWYk8W0ZAxg0(B< zg5c{!K?bnY5AV;cA|0p1h63)zkOF+Rl$x~V@&40ph@(a;NuK~@Cg(*%+i^+UB*B;Y zVok5y;>er#hIYj(jC*-*Pof`6woECA`4{%8SR|QpvnjEuO?<>v0P*dq=%tqeF5nE{ zh+{!VJuZ8UB&J8%)n>-NPSd2$kj{5 zYMBynL`BDw5p@fUzBtIj>hR;V4Q^o z#ir9fS7&rePL*#T_DTwiy~Pk6ti8}Ii3X5VJA1gSx>Bn)_Lw}>8r+=J!8WW>K5^KT zoUL4_Bygo&(6D(JNuk2!sPlaB?~()x!6`V)l=O7e-JFoq=8qXu3h3rvG+tfagjg>a zpeRF%9r|;yH9i!#$xq1)r~B_Z>){t-BBtmCU%pgEdOJAl1sM0f!*6Rw^j{X`pSNfc z?#4&xp)Q^5j1ag$t!eM-d#N0C!Cg7PzoId^)47G1gzqh3LX^(g#J0T$(+kN5q6mfs z+d^6;_bXKy>?We?ALx}&Vp5aChYaQGjlALd91IBs!{h6=^{H7yi1d)KDC2A zUzK@SYEiB!IY>;n>gv?|Ip#P~dTK#aG)`MA4%d~;AdVa1uX_D-f>$K9;#ypO{8q`_v1w8v@iUAM)PO zyrCFeDqr1cB+(CNeD?#Q7mmexeOt4KZLCj_0U?>_I zj>I~n-6g<#pEi|KE2#R}qMRCAP?oVXlGfM~84tYtF-yh7Ps@V1CDoWmP7ixNZEo6+ zoMQ}Pu5mDs1KIcBaE|ON+Tl2ffu7ipNj&YF>G@sw0r4CQx8A97Y09|}MS{a^hr)WY zO8>$BhBx^f_Z5J)a_Y}qBa-)Y@7eeSP+zQbYj?7GHMM08O*qREW{q-4k7dw{< zv8fT0Av7kJPE4l_vll-je|hnH`z#5wr9>EYz)?p)-k z0tk8t6D4<(z{?vgc6+0n>ZetK;KyjwFc=sv(_L#M(ogBy`>~5f_{QNO1g*U6d(0EK zvc>*kWCgwYW(mZF!|9230497uiCuvG%9Z!>ktUk8FN~q(z9q-}yxYCnlnKl%WV9lq zeFFq2TnX0{S9c-SR$$vUjnJ;N7_||m+)}U(d6ewWY)`CEj9KU^Rxj6HF%;6?G#98+ z>bN##u+n2SYaGmFKRc^C8DBEdA4?-{y*I0tT@>>X)$GDKj&}%FoEWRd+maeQ;OdN@ zhcMW2+=o;5C4MKCmY9E0vH3GNB3FIm7r?~u8H?1RBM$+N`^3NVf%*p0|L?);|833w z?P-zQ4{7bP%24EV(--Wer`;v-IVP!q{VmsjS38~Omq8Q_Z0qMy)yKgW!u-mfRrdAz$D;@2a&2=iIM6aDVYbp#nd*k#wMCbU70~R4bqH=zuf| zC2x7@V)#vb@vX z*|Xint))`xmVtz9Ait8aHwk`BKfFFuKXxtVUE}CNq!m~>4NWSWJdjCJckVDkj~{-Y zJuxrtW1y8l=h5N*dJ^h^qFa0x#iR9MN*;Uw&d)CeW9*{#F zV8j;WJD>{dP7))XZY^#wFGQeuA6)i+^tbm1`09T|1Bvh2DKJ=J!nV)8TMkI&+Zgn; zO=ujovCE5*Q5g;!8QeSlo8o)Exf}(@ee*+7SuJRr^SSm2yeB5}*ZC#7fgU*0hnTGc za^DW=Y{^s0GIQN4yWADO4p2#X|7OOkM^0L|J|DHt@?t5&jS7ejc;o%^ic&wp=lyw6s{PfA?EkAm8kn`bQ5TU#bO$BrblU3ZQkO%z-2 z5C^@H4Iv+;XLNP8+^gc%7MVY&Q=!9_w3W1dgonm6o4XIh>m?CgXNQEwL)^W$>D%1} z+*@Z~I_G3cR~okRY#cyszU`xIFWUZ8^|(~SJ11(V^C)TMSyAK0=+OBYLmjtYq|S>g z%@dYnN*|LeU#)I_b7d`7k++#k(d*rwevdi!Xs={y3+ucF9RA_qNY{(cUU}!g z&fcDzd-SS)?WojH$8?}uG48buC#X4Av|Zgt-#z7%JomC+Q@~Vw#pQA7%c)lEeoZ4W z)$&n&A!9MiFCK~C75tjGKd7}^#UiuGyxq|<`m4n2+3R1keE0vuSxg%{ zesG1Uuj4Fnc`+3zk!@@o`F7M!*r5bweN0XNY%UEku={#_w!-!%QF4!-eW z2uFSC4-V!yNE+mG2$E%@4m@6l)W9-VS@bvFcy3>sEtGD|Pz3Oy^Av30Soza2X$f*md0f#}z8|o7}qYlwI zVInICAaL0C4Q<+f`ri|+@bw}7wNCo8CH_~glYV_hfBXNp!_ea`J9&{WSG;9PikhsV zJI}ttu8D2<2Xl8A^q>KUznAdMhx}qEI(!b^81@OEp@AqurMxtG)v;uy_Kg1yTt7uE zb%?V#N8`cazw6}mN1%zibF#68?_u3=8Kd}UUKRe_SWrgT`f>V4c2lb4vas8{#d&km zPt^0^CPz|(2YrdN!wsb8p!D*4i|@6NydAEgRxA#as%=F z-g2!+9+MvLNftt|u12P1m+5jBbqnfV5|Pi8gY7~geXH=xWn0uL3{~%eT3aEm8$vR? zt3;+)245Wlf}j{60q0BE+e6P!`+&J8T3hGk0deNQBqheYDD{gKDuY_mPVuoQJ@^{h zjJpu#lVyPG^qtI>MU7)9*~9Swb+00mZn7an@03@!3x?(L;HnVGG|HR&ZGYOgw&{x*?pabuJ472<2cMs5djoijg$NE zUa2oZDacke?l;pWpr1;N@xD>LmLX`Mxd)T@2GrxoEak9LVFxv7h%Y!W&hg^zlpvY| zNMQ9(t(GWfOa9s_71Y9l%@vuL&vb+Oz_VqYCD-3*xAiAwGS66^ z&8WOVU)g8yQ=6D}E~fJ~kDR45)MCmY7Zc;l!@XT|;c6yuK$ z-5Kf2syUA$Dku5XqmoRmKN3P6=Ylw_0_9)`9F68gZUVIdh@g{SBN_yD5L%N<4KH9) z{hqyJJ;xtep)L=)UwOh~z%jW@H>vnNNkN(|i)_+l&kqN?ti4LFP{!WwEx}fJ#p+Zf|G?D_ziu(Ts0qK-**d{0I2z4i-J6 za4K35yCDNY_vN(S$+|;w7fUde4X6DAPoH{!ONSs31ML8(HQM=PiwQPv5cG1mNI308G9cZt$FZ0G^-qYuaPhij$>UGfmie$ zF-~@4S##tx7)ypD5*b{UnGEy@j$#$upy23_LHfXOkh=g`A@dNvAU)#^ldkvBTfX)K zu!ph?HF=fy`dIAHB@B$U4|8XN{N30BEJH4i>Lp=s*IE^NsIWHdkOShUvlAWov}Rzv zTnhYc#N2EqCe{dR8x;G&hyn4GW$UCQUBC1BOUK_RNX4kMh{rTu<+0DQaoux|4$W?k z#eqbq^@4T{PAFiO9ToYuV;Q+!en!dI7Sq7G{j&@u4ef zyy>VIYrkUI(CD8H=qR)x-M{k%=R-0j&m?fL^MD*>ug}nzL2_D8Lh7DsO?581RY^7H7ZMRqHyY37^YWt(pWR}2riXfH z@hmRC#Iwx*^lNfUEvH{?N;3R09+B^R_J4os|5(u9?-=ZVHfcHzKZSQ^_7xP?)XFQp z*A`3?JC%}SGt=TBlF?s;S>}bTT6OJ6SGg@@e77v|tuDi{Sm_}9krYbMjER*sYwTJ! zLwcAH5x0DypD6!ow7M-zaK1mADeQgY$%A{v4hXYkDM<9rMwcy8_BQmf2lmCdjH_-a zSUJnsSP#XCQY2bicG4>H%RKZvUtM#V0nNnqEspFw0*imk!JgZ*Q&QyN*cv)6Be@#A z+A_0YxBGfysGz&h#kMhU6)-XIEr)ZR@#@&XM#nX1?DiblvQM&zQn$5l+@r&wcc4m8 zre|OBw;Wnn;=}gfsu0O30mAaiiNdR4&0b~|kk~1)u>0l0!OZ_vi1*4T09T=aG*in( zoF$60xV<#kOnr6MXaV*XdU$BBMu>KGi`q2bf%It@e2W4{cvb{=+DYzDE?JZhrp~C? zX2ib_E?I_mHY@Njx!#2{9x9>D7cuw}h-O5YeVvM3>cjm3FLR0q#*9*Qu=fM$0ed@HH*%eEhO=I=LH;pbG_uY1y>qHHom8-Py8;Bl z6_gyu8Q*E2gjG}kRm4p*$GjjEJs7!-q;e@tOJ^(ONfSOsp*V23O&aUY z4+fzW`Q|$vp@nLyWV~s6AEQOWknTFj+Lu z!yt*57I-J2i_HE`5IGR{5A&(g`*>~GIo(3Z>RCT}r%U)ZKiWTAm-E!U%uot6>h;Wu_(|z_G)%~ zH}hFl>KQD^KP`*z1KgV16l_gshPQ~|o~OqwcoTUTpmuytlz@0DYgROPK>vPeOALeb z-M5^ri@CAck`umNFHwtH{$|;f{Z}8_kjp`gUc3@w*Y>IW^icZ7ZB$N~_6>H`+|H%T zV%o_u$LzZ$n&tX2(*!2h;dFxsSK4ygS*+W-aM#9lcF|e&Gnqn*y795R)i?)rMS>$* z{f*94Ib&Y4wAPYSu56x|M0A%mXrdoyihCzFcaf1?I;4{N;7+@^B#fa0Oc6KMU{jMP z^g8HdSKAO%;@$57+D`U4C3&0^m0VT zsr$Bk{pr2Hnak=^?EVj^z)^plfDlPV-?%MOrm9^(rS>4}7XJeo3ItT$)(kf;fpSYb zzRI_-2xG`r?a;4nRHQqSH{@6*|j!j>^=VdMrbjEV%)O?m*JLw0n_a1(5`EeV%2;P*pvkH-s zV{u}90)gp2I9AzzH@y(neQ%N)u49IsjZ-h~_lxV(d5f@sN?uGa3p+U3VsPn1dx|y- zYgh|a1b9Q4?4&-&8q}k=t1MJ|;0DR%Oy;Kil3f~1iFz+Bnj1#sB)`+9fb*VfJ~+|g z+%*Aq>+EcUA^KCImNY~mbJdDtRur^k7<>v2b(6QK(74|w{v71rlDZ;ZtLUq~qTtMW zmZa7YKyk~Q9zXb0m18t>Lcm3?ffVEzkdH~~L)Zq(d2hdsZ%B3s>qQZmrQ6Fqj{t2y zK0kw-;427BVj&i~8mFa|w@VW)%lCuy2PlxMJgdSkj!7#)c{&y=2a@b(G?4XU$zDHB z@;s)76aBroEcN0p1~}+@5A+8+OQ0kaDd<3LEiITF)@mLMMEmyU_lmeE^<&Qp!Ku~4 zqTYIEtUM_S8J__A^H&@{+@VZq^xazyV;}Rv#0Qtg=ZQJY%LY~GWH~ePdH1K2)H7M# zOSzn+!aKApc^u}_AH%o!3Zg6{&P)zkZ=SC#Oz22jy4po)$TH^&Z89#5Bu6`1mUfu- zy&j_Y1jk1NO<|2Y-ks_;Hjgn9zZw>MA6f#Xg??8s?Ht&JP)5QNGG`h$Wu|ms5{ky` z6HXn6#**-NUuxs9bYsGy3&4%*RJ4M8@A3}L$4oaP&S?zN(Ot=f^yRWQokiq0Rlr@$ z%k5ikIC|6T3$%|rrTAnkD_7QHtr`k@44z}wI1$c>OG7W#c%2hN;$NnPL0RoUJ(I)> zjT>~Ev7P69(suUmfjS2i!q68ct#UgQ*yG-d;gWo=^k7uwG5)tfJh|UFVaLdsbdf$SI_zRrFob&1Yzt zR0|vA7A75#N!sgkDC(}hj26XzvT5~}%dlOi(Z+E4wQ-!Mgza=(NR;2S>UJnwjiHhG zXkxhWTfB97@lY%MjWucLMx&>R`+Vm)2a-%|(W-hwliVtghw@P)pBMzFYt6dj68-mwLWzauw@q^xytgH${4j zMsA*rc2Jj2W(Twq8&i`V#pd4|lrtQ-eBa~A7{?Z)4EG@!{ss9#6(L{`853K(tg++T z;QL}(yUj{NoN{X>o92g0g3*{vljd&TBpHUq*ajR-LeMgA;ES0wHJPa#5Kiux|?t9 zDe@2X$YR>a2PM?D4JZDviJh!?di|MNaJO%}Rk<%!(u@lqkTfoG*8#$F+HcU(uB^P| zvb~>eQlXp$HZj>N{N%VD8$&YdcED4S&ghZk_sdtRSaKNys;Z&d${~ZxvFQz`1JYZX z{IyFYi`RfcbV5q+znds91>|*nje8IOHirDu5B{1c(6)j9$?ggpvUeAX%$YawOjLDT zGAp9aYz_IV-TBAQP6|dF>@BuzE;W)56BT}0)**un%FHiWjvdeQ5kxgz{sNss9o>nyuS)+h#iJXHa+^KUI4(cAHRB>sJ{5h z&}q{Ztv_G;m7w!;&iw=@I_?(#aR zvh_4>@=X8qE&@1+@$pXpDT4zNpPhfJx@pe8OmN5L`(Sib)DgAXj=^+))oey|YtQ9H{$zB_WN$j-&AG!tDVWP>1qdJ2D)ob&z8WISDaDT9wG6MMo*PDTBv&?xXG(wEAX`8V;}s&b+s(xR@p zlNFYR_mju?Y{D8qY_U`9ExBA#4n8=XjhpMpbM`C0k-YC41upl0fmlV%SS0@)H?Mul zbJrWg($f6bYuaeDxCa34 zAM(NfeItHtFk08YwxR#l-qM9FyP|GO;tn4hNXhV)dM_oY+4f2n%xD--h}~f*hg=m3 zxbyp!_}eRcc$KEiT(?K>vZsQ=%C!1(#7Zv$W2B=!j1oIQ>D?uI%Z*HIxQxhd4Da<* z-#2F(B*8a0gWrd_{i^bqOsLj|Vv#dE8r)SRl;*MW32>t?Vz_r*B~1;rsW>$O`=_{{u6`?Cg8#&UdQv#1@1 zTF)+>Ij+v%WA8>y7UyvqIk+b2`)aiS!^V2xQ$!5%ygtBd!bUwgabJnJ}+FhdyXNXE+rdWoguikkrEKr_t96Jzpw4- zu8IfK$Pn1HwBwdVnlhS2=Yv0(WngLu{t-U}VIq_)`;hHc}u`x&suE)e7RVLd{*IM?>fR%evlYO(S;XcRO>YDFZq7PiA6eLKE^N z4JZftu6pvxm`q;%VLtKP3cE(5VQ*vEx6{s_W!&G@qRF^E{N@t?(z#98i7g|{YCg<+ zwivRiVA=n2%k&t9cREh?DBG9v#NM=(<$LLkPw9#Hh+}G#<%rINho5zZe%9&4wx{~( zCqSo@jDV-P;EY@7IjiCAl${{*+Qn||fBXbUpY%ZPw&;I5%}LnRtjPbY zR6Xgy(PNtdSjx}$vgl2d8=jEoURg)vx`fCpm0*iy>CT0F@)d)FsB9M(Jv(hTEVm@s zq5}~d8!jWfHn!dkBLivUer5IK z(b*wq|D`@M8Q46Dsj0CwTn@VG3LWOK5N@~cipMG-!c(1FBdJ@Lq*=Y#Hs5C8Gb6b3 zP+Sgd82~gE*ewV#=v!Wi`8L%a)Td zH=Fy@4pvihinxPdBdHh~s`haEx7c3smo=hMCWO+K)iF6tJpa<|A4l`*uC}5(wG&jf zSlhv&*zpH5m2N^oCACDW9U&h?pld8=L7IP&1wL$yDklxambbQcikX9uG-|41BTfiJ zlw$!tvG-@Bp_NPi6Ye=tEXaU2>gsQAPb}_cB zzY}S#sut|+p@*lmg~9!!H;b~J zSeI9V4AqPAasB&ea79iA%Z%BG4rA?!Y#fXg!Id*+2)|CvX=OMQ zVHTK@Rj^)K*k=$l7oi*AN>Q!j)Qh7uEaRU0#a^h*DK#5OOucG1AM>Ro#r+x5Fc&`Q{V`yiM5Fw9UP)=x~nreU9&y?2_V=rPKfCr)i{*Ewyg3A@Vv=Y z-7q^_z3d*Vv?aN#^g-k}rz>Y6d#vlCQAc^KrhyfVu-FZ&bt zO$`+k+drcE*cLhjaUt@QS%V?IW$Nix*cjGZnf(Cw>gHi@0b|R_*mZ8JEX%_CIFelT z>>N`(bl9C-8T{i~rLwlLZ#*yueL?gAX6&xMkO~%`#o|~bbk&*bZAiIm9*E5!!2w)0 zdAVqr&VKvmlr%I9Xd!)5(mwM+t&~dH>-3wGV9Ihn?W=nje%I4*lYh|gQO-Td)$oGAfN)@~lLAm*{zkw*2V`a2{8-!n09gbrL*IH;+^Pa;XV+)^(cULN ziV}UG#6_t>nxZ>?EAv#x+E&(XVxx%y<=M-9h*$uC5}P9;r=l9E|8yLQ4>(kIj$`9+ zVQ5gW1;0wNy>x{iD;%1|^1SL0-%`f3FiKJ?R0C%6JbgW-*7l=h?EB#&WfA*0@x7 zokrN<13Y5UQ>FrLmQ=3Ct!_-doOYqr5xaY%A7q+oAf}SylR8p$S`*ruHWU*BdXudk zFXJ-rx73sI?;xi~i9y<|`*ZPm?XA{jb@cpXJe#Fxh^a`0n!yH&BG`M|F;}bZmI}A5 ze})3Tu_}(IF+}DNQzN`jmu&rBvqJ|tk!aHhpO@C}t~x49J@tI3`60WCZhMWkv9q~E z-G(}iSmsqqCA3ptuqoO%MsL(YG>#CCb#St!)7R0>JCOrQ@J=s0~ zty^BF?s$K&IL&wC)##w#M`&R0a_VY?R$sWA^-TpjJrq^UzEc(r%tjYyFlW|$1cR1K z$>?aPWs87LoB7v(vnP+jsU-sv8k*0ed=f<}6w@=t-+1y#VVi=e z%(%wPzTwv7AgYhM9)DeV`r>W?Cu=Ee=5jM3^MI6f= zx$YUkhA&&dV309$WZ-!K@HcJkKb-u(74h>s#`_wt68zO0>!Od#%rAJbpNG6%U%j1! z1fy5qz6a=hCuR>KL=9;Hf~&vu2{EF=56OWPe&faeP;KN7?zns>7_gspe`J2oB!2c3 zS=|UXE+l=M7rf?>qafk%447o=R&4AW8IrP+FXeGx^wIYv0genVIl4ci++uGqqgV2! z+oXtEJ6TuUh67(7ePlKT060{jvI|g+YE*_kdhO&@XpbZ?{))4d0>XDDoTvVsoB{_B z=$NQZ>atbO6untH=DO?0{IH>IL>2quO+#U#ihrVjLm3lHtydO4II+&LUM%F78Lci5 zDDY73YGr&;0D&T7Yq|`uT6_)I*Tf5%*w=L7^JdYp%W(=XfByNU#(NeW&cdeHA)vg* z{p^f}N!>8^|Jo!lFekM(4OiBp#~DxzTvUTb-*piV|ATJ%JksLF#R>mzSnr8^kA^cx%f$CH08 zc>UA3)lspjxN1Y$v#EuRhOo^atrseR2vv%2KreQ%%lyZRj-n*ToZD^3Fn;=#tmP4> z)A?@eU%A(SiaD%>C~_W`IRl&3EkPuL1An_L`wyQr-GQG`=#iG2XyVT(^z^1u4yxmY zradPX8mIU4OQ)mqE5YhsdPz1SjnGg`w3vVPb3hI@(myS(F#wB8wcA1ScNLqP08;q>r8<}?$j6KT_(V^j2Gh-Np!=sV;SgRG^ zsk3+SX-=l6l2gdO4NNMG69u#SGjmv^%XaZ4DRo2i`#c3GH6S6%0>x6M#HgP~BXW>~ zK6~=)(Rw1~#Ee-GABd?`0cSIg9C()Fc{X+MBuq$O)jkg-eG=uq8}G2Z7P#T03FCX9 z*x@8#Lo)v0P!)1XUDXxN038;Wbn7r6zZ-R_H+<+`JlT$VOiyR9 z51Ph9Yg&L6Yp$4<6vqG?f0-K|Glt>UDm=|gX#g*`vjsEr$qX|^gx+8|RqNr0S}Pzh zW2)QwyN_)sY`(5ha+*5Dx1#Y~V>^BbI@V^Il&8fl&8FCCsJ${co`cL$;xccQ2;Zck zqqCsxFm-F2^#JbY|L`^c4_y2!GH}B`1o<^aU$)e;?`P1YUt9hMr!TAZb&^Ho*~^>f zR|JU2L*bfgw*EnIFf*5ib2Iqvxfj4S!G6E@6$-MyF8fc9zanyLKHwwhS#P!)!n$-P zGhX70LCcHHha-DqTGUI7aCaM?@hnfNzXHGhjle%b$_{udeF-F-34Oqt5Y_|4t45{^?&v?e>@7-sxO*qre}21>CXx z9&qQ#{BM@oX{3i`y~SN`Xn{qHUD{a{_&NA;>(Hn268X=rf>_yllmo3ZvF@8+zB ze28u2x6tZ{dV0XpaPaT=5Oxd~E&f z-J{cmR+heC@stHLSkVK<-~U{2>!jb9Th$f@xcQO7m1OJfMLo{;drGzp;{(ib;s8F0#FwK9Ff@7i)e- z>&~7hx#w5U9g5X%m~c~(-tBX6P8)9zzyZDeUWqxcvIX?Ef{R;(WBK~8pO*aD28RFG z0+&71AR8&E5Lwm=JD+&#tKphZ*qQg10*zejJ@ z^UcPrgRSw7*a$28E)UI1wUjRU)Ents(m;G337i}^_8`+V)22l%Zdx>UIYM*)(V8bc z?m&Ar(|Thk=gjO%am`g0xP#~QX+Z~LGqHSaMD{4D>X-aoimURi)crP_v&Iqz=NjJ~w+^q}Rr58JKcxkbA%`EiwNd$<1R;}$}T zDk-GU0=)c5(vbz_<82#+KwFf8Oy`WCFd;yiF81pKXC0?uZ*G_EveG!=b^w4kKPyn) z&2NWHHgn%Lw$`|tg%Dr%ur}$Th4idQD#cEZ1S**W>;_otjU59r6DuV(SBZzU-_CbjE_+qd{|WG9sef#+ZqvF}OmbU+XFDQ4 z1(t;!D`Sjn zVOz5{x2|kGU3a6C_Jq!)B)6lL18OJZn~=08uuZNjB5)>5I!Bx)*PZqH<<4j=h24Ew zI(6(5z`j%*>TpmOE^t$h>d{r(7uF{+&Um#&^r{pGx~Kx2K$;R$yzj~NrA$8!WR05;z@yFA;5w8sS6^!~T zyf%;;d)@}}*7Vh!cRNLyW+t}v6bap_)DC%lf283r!GD?Jueai_2J%;5@mII`pZS73 z`uu5HN80wC505_}CU1o+yGnGra2~B@!62cvxms==`RwK1=|{i)YW|h}%L0E{;4cgO zWr2Ui0`zDc4$YzjL}4-U@jNq1mvZEVeYTqy0I1*(ss+y-`5NI5d z#TFAv{#O+ImG@7vz#gC5w~yF{D{j*7(E7GmixQW#vK|{Hs7-||8QD3O z;(rE+1L2<$z`88sl^-PEIX&dg!yt*1k<^SNn+{4?eHTG;GW9}i!Gsc!!&m62eEm#l zSZ5yLdBuwM?8o>cb8qx1x$TI-W$pMNV)#~A9TUA;pR2o-+RkXuAPg)wuBI!5L|Mx~ zRcm!e{h}>7P~y?DE5PO+6b%KkjP+6aVh-^n7@<w4?q*(VhP;8u>C4nYUV7l*EdcIM0W zbh*sP9Ok)?e>xn?>-NKL+GY)Gcc1wr`G?go+^bv37g3impR%WEDSA12 zN#>Fy<(loHd&R)BwXiOj0&Ag3{2_C&*j%-(o(=p%dJIuua|CoC(W{XSIJRmWrM_aD z+ZQy6sra$%^ukEW42xN#l2OOWQ>8k_CdWH+v*nZS&cbG6D^U0_IJN#vCSuDVrBLC1 zL6-ttb*E3YKg%H0UT`(%flC;HVysX&ByANTu5c2>ua-AqSh&F;lHQeM>MkXos5Lq$ zrt{!#7uw!eOklDT7r=wHW0?`RH@3gaT?>wmlP|ikiQbQ5hdYI3hAX4HwXUy;mzq0L z&{%D^K#s?5?fqD4++BTcx;z{#(-Pi5FRNT9&4?*Vv?Ij^v&86G%$%?9TCp=Zp%`T% zfT~pBHE6Ui?l0?h9*MKubj|87neg)$JtZ+2{}VBnAeln$C9ce(lXA3NUTnWag1E7v zWi~~G+XLw2G_0aYnx%8XDW>`|0re?hAY6e%kXfRiF0YQ57ShRVhNR+6FeS6S7?#L$ zEDdFnG*F)oFJMv*L?D9@7Ng|MoVKKN&a6K7RxV4GpsHQn(TfnyZm9&zsZ@HpRSXU@ zqoPSl4JgTeJRRZ8kYikATRRRkg^1q=n^w14l`a`X_F3>tL|7~IUKUfgM)wu?dH2Tx ze)?0|_+Lu@kOd^hFCqB7k3P;wB(!&SlKT5Yd$7JH+O-)2xFlXWl+~{AdtzF09x5iT zS|Sr&)s8Z&@pqC*4T8Sa9cG)qV$*AaB7 z+~5isMoeqZmh3rSqW#`cT>mhFJE>cPsn%Bu$CUOs(=Dhnqk?Ga76zB4!fv?{1T{Ya zmLzxgXeP<0YB100-)tR~Ag zFhuyB20oDAMR`g#d`c{+^EgT=Sss+v?s8nZFRWJ&mt+egqj+Z4Z@wW%T=d+adHMse z8ww~r8baxq%l6&`%3jXf7CR7!(HqO+yp*pM3&E4b30&40TQ=p4&C|e9gD3$yp-Q(d{#kQ(FBA zpu7p3s!4y!S7alI}rl!w9iijo==I_xe(QO>4m>umy7j^a!g3A?WDDF#? zJnH5S+(>k0lziZPbv%Fp0`-7s_Rgq;%51lM0z86lC;#dxKWMe*K{tW|@OU{F%rVTBaK*L)+#a%W|L{x)8XlN^X z36h%)gZH-nU@yT?&cG;1a?ziQ4Z7DzI-JqotZ_@Q<%~4r3Eo`vnvIM9$-_Mo%v>y9 zRQlSbL>dnQ41qkG?PXq~m+i~Ml2PSB1QMXq$7-i9ShSVS751$r7ov50;)7wSG7_yz zv8(!=4*sFNn}$bn&dxJmhdMbI&mbTcFt8iToK&+|JZx)sHV2HU7T&-sJIc4EzP|_T4)M{DmDl`KuAI)B)FxB zgdVCOuyqSc5D+9FT>^v@5~KtOp;(X-q=h6%2O;#LND&nG%{lje=YDtGd(Qa2-?-y^ z<9El&A8U*`*PLr6@0wZjUGIF}=b>ztNJm_|4C7oGN!D+5GH>hZE=f-7Hb>L&ZFAng zpMDV{Dh69c3_pwcg#*kIHGrN==Y3{(4C%bB>sG9ll!Ux@uei1m_CEAbZP{Q>$bgkk z^l9gJ^Te__qwa^wk|yQKSRQDKpD*Hi&z76Bzz2KX9MG#B?=5ctFuxS$&MNlk^Yjru z=TqHM9ZUO;L>CY%U6efyT;Co)Rb}Awm`w7+%zk7mRP48%{i8T`v3#PSeLZh2!#750 zFmq4N4DBEX(&#ffhcoX&Wk*dxTipsfGZXIyxG&^}%i@wAy=Ybf&mYW8Xh;{>4ChGK zV|*qSz3!VwBVLm0hYd9gh7SF)an0#{&7N(xZ~cHeMRn(Gl^)Gxj35l(Am3XO;t`_g za)*H~E0&ZDT2_Z6)Z)i+zoTAt6$zY|BA??b?B>~BjsEeelr+YVvcVlROJij~osJRn z^^`>1)!+JZwjQD!VOH2v9&D8zo?|~9BLg&~q4vBR zOJo#{60KzAo(Ksd*N~X6S0^4U!u5sh$7;T4vI}wZIF9%xGL7X3(o56`QI?F(A}EAv zM`MX0^)1jK@5Ouq9F9L3vJ04B=!hEggCDxtY!;|f~e=x%-| z&EVmE2!M%siurO`4{nR_Rk=85FpfaC+q}oWif&%~3jV`JtAv!z8R6?&pM3df&L*|3 zhYcWxWVor-6IpmoYe{BZREqto_z=Evky~3yPg7`vEXlJ00TWHZ6Z4m9|AYWEDTN1zYlgAVY-VGoS$h>D`EIrYdJ+``ejMorxWmHSQzJRujCtDbOnXYQZ z2%^p@0JF~4R6^-KF~5UNajCl`r4?LIgVK^}Y?M5YeEGjWU-#QjUC%fb zNOJZshU<(@<0g4T?kPz_P*!$CCH}(4kNd8lJs#r54MC+~J_=bM3xE6HFY&LMe_#Ql z7FAg~QqQJ2;dJeEqn}4guD%%Q`B!S{B#!LB(OI6qlG;0IRR?~`n(b)9bIc0I# z)U2FT)7y{VJZ%%~4=>9~KDzIN@o+=_mv^uVhqGaS9J|{xpVcJ2prrx&4kg#Aq`_@9 zRg{gDar#x7x&c_=`MeF(;uqcyT>sq6XN!(8Jcs`Pw59hLvjY)Hu3K*@(!!n##vDV}em#yh*y(nb~=?f91wL;)nQ<8MAAn*EhYYY}C&FDUxXG zxJ8MS7C4tZDH+5nk~ZG0)z8OR>e%G&B%XcOF=PK@O@3FSnE4fxNkiuX0b#+tK`ALi zJv5`b8q2Rz@=Y^+`aU-=sioo8YKfbQe<|gDZ6+YcW$5Bb##z})rtBai5&#H*?f>l` z+=YL1>VRHO3hqov;sW530ac0#{(=fCG_4lG8a7)Lzoeh2gIH}wFZ>dM$5JYZa0gS3 z=U;73L|@S}@vi$*WGUA2lia_x&HbL~7*l$<>G?+0A1kV+D{!J^*_r5^V$q{jP3H0jC%Qv1m?<62wejn==2#clhpG^-$s;l zTMcYoV^jBXK7|x@(=+x0(^xwH#|gKpM=M73(95cb+}g~VfpN`}}W4QwG= z{bu>SK4y3M_*h9BRO>??^d$&mtJ&)iCGEvPr=6r{zq3@h9C#dA;!K1#k|!ta5W<}Q zW{wiQ(4KR>5a8F1Mb28k$R01Z5RwRvz3Q4oFBnwl@rD;VL0$>JhltU%y%(MnC zzw(eg3MCob4WI7kJhxd7vz0unVjQmeGryAm#S4bB4U@lD|_Hh^sUfaN|>XlEXm>#pr%hXEs3y%b6~d^IRo! zwsTX}8<1bEEqh#VyN6&@=C0?8vf?`7>`(Sb%B=T4S=aupiZ=qiUfWEcyGzErfdgH) z^7jwOfbG@s;DQA>+%@G4OnsY@^ruL#4Du_mA+w<&Juwk12g9`$pB5r`q%LB&Rg!Ii zRI3{s4ge_sy6VZ83*j(ihl_^avNwgk;hTpe@!+r_&?<`jm>`ogZ9ZPg3zhD*wzePX zRtSMYd+NPx&y(9LL`lrH<(m*>_K|#u13Q5_5SED?)4t^i1e+t;UB-Zrju}{2L;vxw zSt`KYtn{Pyx%KvJTgHCH5Lx<&yXm7@ARRXEOxMXX2yQzTw%JIvbUp1*iZBF2fHaJ8ofh~YQ zkJhWD@@1dTM|&GkLq`pK(I&3PvZP(A<_r=B`a=qj=8SH9>a1oD)fX&NCn zdng(C*co8Yb*4hG0D$)KF|`a+=GYFr@jbsqFB(M$z9WF$-#8-*fx*pJ|@P zfUlYXWucso#oAZrWT(X!+l#ZrEST)X_^Ffw+sX}EOE=3|nB$guPUG}-#Zo`lk)v2@ zRni0;+{qLt9XMMw^f(;X09S`#l$cFbR;};4y6`q)x@+PRIkfr3%$y^tl#{s_-BwtEzqNeW{{30|RU)Dh1-%Z7#!;Fqu#)H-Q%mF4_i_E( zev}n$YT$u|kC{TTaNywB{)k%Fn{$hczgoPNA8@TF4e(A>yO;p<*Z#+$Xo)^er*l=g(*8WtO8D znR7grQ?ITxg>P3s#WcGY?ZgG`C!D&R5}KEQ;vQ*6+8&|Lw|ua}^Bs{=4^Zb;0ZKuw(S_4@D!ULinod`sGI} z1rvXY(00zYPW%t=;c#ijS)Z$Gw+}t^+xk;vFR zZ~Xu9>s%eH$Ox+)`YkDM1WVKzRThAB9fwF%+NpJG=~_#J==b$*>K}9_Zu|UF-F*($ ze_@{XCZ`o-zi?z1d?kbXlhZ5vgJcixsd-l_rF>|ESNpp{M6isUlspugU>|E z8MET_x%P{8q0_35k~kz*zvK}&n;ms8y6UEU$-}C^FtNF5zr8_^xmeEJV;qZzcxdsB ze1ivhLykFFO9i`zKQFe*DUTqBzY8e**)1M!?pD2VM5yAOwN&hvc{HBauR$Y}r5AZ% zrclWkx^Dl={{EaC)0?FWfoaf7`FPO{tAZIUv5VITU2{lryi|vL`q9gBaTKv5w#z|Y z`W`oUQS6b(zB;O&f3eH(*wqN4B}L)0E<^lWd>3ahG&Eqce*B?)p8=~y*?6tS^s0h& z042@jG>YCOySXYa%#8iDiwP|n*_FCnWG^?LTx+%qvuzyHiRVfz#0(t+n}>?8n1L(%qa~0H)K(FHemk(|0+&Q?tUOz=qtMUYma~kZhK;XD&y-NsmSmS;SeJtn5Ch1Nw&ZE`9%vq^EMUZQo?WNb z1jVks-XP?k`a!&mwtWAxX!#NU`exK$d;O)Sh%@ke zexdX2oUd{kpI!kG!0L0%A#k?zW{<%~OlHsl`@A}9?}5`)RIg_`1oGB?yV^v{`GPbh zdw$gRul@ht;ZPG;5AJAuCYK>onbD&g-+SeLdI7%v{%Om>s!>4h-|P0T`oCiVvHk2} z*n${b?>i$!t^s6Gg)RWh0*Xh*;mIS7N4XI;(!;*^W0E!LwHTjizqAhC=q$zS)MFm< z)&#X?4@3~ibDh>aALVl$9Gtc)u9dyU2?dYr&v*!RiqmXaHQ$R$(&=yXa9@;Ci(|K2 zsjTL5)Xj)h_R8S}5wq)Iz(SZ~DLQBBIP3=7LOcBJVhAD{@%llBWb^;=3C48O40c<_ z^O@sYW-j|Dql;m~7EB6`oJWkvX@J^seB?~PUPYOAd=_|%gFU1QbeVdVp}D;}_rT<# zGhQi2@-?dT4c}*nj4vxfY4FG&o=wWup*?xh#I7QL6J7hEw9f!7NL5P+vVN+HhtR&$ z$t`JP3g|i6t5x%@8$Anv&tP(5Zg87mY(vk1XhN0iJpq3?K2!MgjNa-vfqSI-#v&Yk z($b%1Y{COn*rl)qQCV{8PZsquKK1v%vOei<=;7yYzpgCt&QC;SZnSpqc3@%upCUKT z^jXoa-h9sG+a!V52A!6W>Ks;?k|kntNv3&$S3+$>0$Z}np@%a!94>B zQ!4VS(~3th3LGdbj0o)dNV{a)_E*ZnX?bfF*b#aGyRLK0hFC zG@gjQ0Z_g^5-m-gfR(}aBrh2$>M9v6B zJwjfY+T2yi1S|RsZ2QvK6UQEt8@hVoMjZ9Dp5c%>g^%OgK|=9Lb;zlxKTuAlEBqX1T0vcij~#6suwuJU1ciQDW2pISOx3bWUWdj()YQi0+!1*xVgJ0>M3X zt!9_EbJa~s?AHm~T+YhF%x;wp;}OfUp9oVG%b|r436hqPquC~3_TRUjzt0-UdCa<9 z_Esk#oM$#QF&XNcs`jzhJg?g5%%LX;t447xQNfLq<`I*J_s0P)k|xe}zpwEiPp6q= z->_EyG%*t1b&=#@Z)ar1U%#;1<+ugZ7a(J5(jZWZ;U{VBIHSu~MARD0p86XR-lx_M zgfQygPyy$65H#`Ngt9xN{BK#m{8t*mPO$&^{=#p(mF7YCj_Z2r ze`#ymjFph6%27>Zh}|ohH3PVFm5WSGngX2-P+0B3GEoFYgldyC^*PhpV$1c8b<|++ ztaF-Co=~23ibdam?e0!^wYaFCy#L7}nN)h0l|u{fN|PnwNF)+2Dc)W!dB>`Q@erVY zL_e@WDa+aN!Q8yEd33)a6FJscVLw~<@^`!SI0{2E3Pl@k<7rBQ>(qyqjuzj)MW7gk zAkwy^4D3G_<4;=25q~M=y$ss%$r%P8c{t8Cus%60Rk^BQGse}AW zj;f2~q7*BNhkF(MKADtaK^J_T{bb)7{&8n!_w-1h@gbsd zv;gQT*-b3VJRNd-P>V0i4S;lT$C@BrdH05x)`L%O=Nnz*GW4{YVvhUFFUn<^(qkpr ztym-12eBJt8Jef5yxL8>>JcryuyRbXXPDkfn)I@_C=LF4JH>kbQJrIpm1~7u(n*w@ ztK?-*57sP`iLNZ5(Pq9kgh{F7WuZI=YMzXgk!PoUpM&EqTaF_XhF_st=w zdV=OkA9d}mhNujbh7;dnP+_|tp=x8M7bdQA+tV|xi8O1P0`g50ZXE@i-CHoc^Gh+O z=93$hVu*^u@PVlA;26x1l~JWsEEkHpl_0DX!Ob5rE~sBR`jR<<`JGI zo0wsLaXREpG&~9>HW^}6Q>a!^U}?!drIFkN_l?m^4u_gNy*D5tD`CdN;%N(XT@Y=^ zDg&-=Pul%CzlN0EGfNc9_Kb?gO< zZ-BLavkCJE{?;EK%i|~+VtHM>DqHG`UA>d>hqjdld^OO^H;Tt(2Ro=G%#r<^eOx6u zn}Gy;XGRL4G>r+Ri6hR*r}t&D_O*wF%I<9y;da^EO>j=fH>0J;QxZa)hQR2n-)O#% z$;vPli2S%E!w8iDe4O!5QPl>S( zQ3iMh2kHhU{L}e><B-&@oI3<=Ja3T_L&AG%JzGZbI|7I}fJJIh^7+al?V|S5LOJuaBMPfAbP& z*<_E_qw(|miBQanfve*%#uVg3${!~hQaz%xT2ng7bk*Ad(zh_a@hg0!SG1(BtI17g ziq?#9e?~b{_QM3I<9qFkcE7sXgb^1-@yZnyU8pQB_o>f>o!b^Fsqv`*8^6^Ix^Y#> zlb5uAGPO#jZKPlR8NvfMEwg(im%qy|yf*I)HoF0WV?BQ=m4)jR=R1xO8J99zGtL{Cth=XT(m)ckv$@H2UaUb{1WBsN#KFP3O!K1?%6ZB` zR?{ymNx`aGrD_=5RZx9kxXaFoXPjOPLEa=`mK)U6J$!STY;dfsMyx+rSR?-S4=sWs919{anj)6rQE2>w`CGQBsZ2mAbEtBIpbC*?97| z2jMO|>a5BA8&`}>EATcQwJsrqlpOBrt`t0PTu7tbPa<7xVi+!F&0CW{3;c>c1p&thOqXK_Pj9*9t0TF*r0NHC$KJJ@#|Ji}6q=eO} zx7R+CnUS0=%jSaNbT>fm)rqUJmN=C_|1cx6DKTST9 zvfcNdBynxMSJb~6A=EVwPix9o5QI$Jx@cR!_KVno#b$KPVuK5LB&;m8NHM{`VxR6w zwK`wH-Ui;sj9Rm?A$k1uB`a~LR}^1ms-y5Rb=)LP^3Drnn4|&+y~5pmG8Odnf~0Ff zX2t1>7|By7l1>*_FJiR1s!_j||M3StY3xE&9&ent2Ke=S&~b-O;1bB$#A!Xw6_&?Q zFSJNvC<+xxnob(*NrKQ5Z0*Ku>{Bp(B_PE!k{TvOGwby-Po6ZYj7AUg6;iV-@i3@#eFGT{kOg+ z{?_3C+Qq4k>#32PL8_+z>!FiAGVvD!LO-_6SszFV2$vPY^@5s9g;jNb5W?Io)F8&9f~+@l!G+@(H?V;doOsuY3d*<8gNR zUe}IJdKnDcL;~WKBo@j!ZS!Xo>b#Utl@f9oQg@xlefoFRyFM$He*NESd~dHYz=RaC z({;g9E`0;5yt3omqBaSwZ$A}e{+{6BAq=)N5+9haDTvp}0XGP`GZp zj8*+)!mSb2GgoSk5s3aL8OWLi2`3 znPJjusii52JfXyhhaXlO%|Phd+u`w{{coIVRxs4z8F1+>iJfQ@>jyo1?H2dDFeSCk zBMyGWxchc1oOXMD>++{A@$<}u(P+OaWO-#r7rHhjzDjFg#CE@jr8aVz;3;;am=UJ} zU68&E_NaCoN|7;X))xh1-Tx@AC>i+FI##LuXk!F6E5#5h^pm z0uS!l%P_a}z-mb~4P;pFt|*mhh1o)Hl&yrp#m1|;tz|}=C1LFJjE`pE{lAnYK51I!SlOs--D zm=ohGi^upZ)oiyN0ec{z5n5X?7g$QaS21F>c+-z2q-$3o7)k2J!+K+`tbbVS8C;yW z@@R4OZPd%80p-`r-wX}o6cODL?gNskjlCf2J6>k|2zaH@TXzqyGZ}0mBsyav13fDC z{gLqV%zW~JU4z>zNbp9RP=?J$fTnlGsGqjmTmg8Sel+w2`s>~auUoW@H=)%c8!YwH zqaMs|O}&qH1(aPgny>GX66JjyR*Cl906UuEQaoq?8Vt?G4Xzm{w{>AytsSf3Y~%T9 zR3+oQJ`A&+)=_V|K^V?bM#X5CF3;-b`XkPdhm}uNrM5v#hV~_Pwe#t9V^e#2m#ASS z$ipBXYb6F3HsVRV|06{a&81mf5Jx?OqX=|~GVhf2og4l|4Fj5*Z7Buxd^WBUWK7thxX<)apue6tiVF>ezOe z(w1)xxckeDlfPvy`^~Xu9L_MvVemrey2_mm3gfA@!&Q5RxN>+ALOp0O)SHP7STxR^ zUnsm*6G%+tQqStuEVeqUUr5d`__=L4NbeWUmMGE~XB;h8D~-$x`PgnP#=hIUXKspF={*~Cq-wT^j8 zK@P&*;<8M?%-fOAkG)Njnlv>{kDm+Wz5qEmNQ8Uflofw&T{H)GJb(?Gfnm;?sNtvq zO|$%(s%9JzTY;nyLnihYVokwtq22(^LsKtfV=$`Xqh16K;c{d(CDHkZ@4n})}=QX;?H8~nMD!W1`^>ilkjos z#~;{fl87VOaE;Srl5q`w4S>UMr2iE80E;<((Ga~@Z~$60vV59qqw?FxWPHc75etEk zw3zk%q@$>EULQO>qShr4uMrM$mIe*ESH$_6Sbw;-k!Lv99QFT<6Zy~hA(M@@2*jf4 z&6|B0M;~Or%K^`NsYEfHTq;UUdqWlh0Dk*K*4OY*6T%(4>iA z-wh|^B4s>}2lbe(-B$3wHlLIDWkCRK{#|jEKVPAbSwgL*J>f%}3oI>!0k0+s4+aO> zO+{rq5eaIt?9k7uUWjh$DQNzWcg+lG>X_6=PP+rD;Ni%C{`9{(px7*Mr%>GNj8*D>B9l;c(Od=;=T- zAd9cSKNV+@_D%*nn8(00IN>;;^XK(JjdHuSAz#^)PhKluM3(z)I;T9#QI#u}z-V@Y zle)cA7pQbaFNBHKJLTBAtt}aH{Sj4g4fcZ*A64X{r#n9qxtzx<=4-i|wm_@%*6gB6 zG@pyXKfk(NC``+X^((CLcQp#)82HlU8nOn&I;i@sJbH7K~bN|-`!NY@9Iaxvy*hrUanDi@L_eJRMcWkv;z&5&ryi~K#~pD{oGHO7D10zY)_ z`1VU86S^IuYs}}yXxZpz&DrNraGB)A*^1D_iEx9-jF~qX6mXzVN&(U8Lo>8a@+WWE zMV0T05~Wh5#!^l3ZjXYyBRaaM0qP{ZFo)8*a8}-Rb zD0|b6umnIqv>aoRWge{K{YcL_fSW`gi~Ds$^=-QHBL=pstmvq~kK@*CqO?!p~ zG?NbNHxFKA8>r>j4_FB@2S7F>-Ok@ltde9<`LRV8PpS=xna;-z*w-%DBniyqhNO(d zEB%A5JlF=Wviu{{%5MIr=kLEd{F5xO|1ZvdXla;cjI3nn>h!5k#ly`?qmEkN;^&hH z)S_Z?ofQ=yCd*FT(H_RA+z}Vc6qyQ(P3Q{YMW2B1uI0<_>3V)PA~s%1c$TZ6F-%_b zhN_BF1ujzWB3>T;wJu6N6tDLm-CZpetT(LAfgPkIoLo(jeoX9C0FeNL70NnA?m z{__2~NT$yKaQpU3*ppFzNhc#|ZvBVRgOrICK9&!g)?=QI**!eX~JG|&#dcYIGa1r+}i3iUgFs4j9);bJ4<EE|ZW4V7W+QWR<|#+~aPEky8kr0X z&nGrHzP$+B6y5MuF}Eak;i-9mvQtYce>L?!ZAVn0*FcsDRnf14P?A}sh@FxxcW0fM z?-9jcRt=MseL}Bu4YdYXY;S2BvP|D~ou#xz>An=Zf5cz=PHYi}KhpT5R*HXRC{v%M zI=bPR{nyKiUaB30E|^{=`u97wJC1Zaz1(gFVOk%38Og8<+;!hM`tq;M9LpOi*VmBz zVw*Py{otQ$^{jSsT2m}7s5QwjY{el85qe*$PlVvM1F3w^DAmf!k!DgUXOCg#kvz|U zwD%YjEzl%M=qdrE{!{W>e;xZ@hree5@ogDiRll?~mI>(xMj8{t+md!{ez)3(N3eSy z6v3^-l+-J?4mIEC!{qd>cXfX2wa|rJklHi$c~$^-|3zx@it8rvSwmB0A4bM=oyBb% z&h)SNFn9TfOMh>`e^@Uod}s7`p@g<(*7U4Sid*C~Y$ePIHzbhW@j?PH^){^@f@Vm(?SHb?JF!JR?uvl8V`c4HlO-NS# zIo8Y3c3^SEn!+^U@4Z;q9yXDF|J?&~MsY5@_^B`NQ(ISL*gf5jI)3=bp5kVO+QoRx z%H^_+_}7_hk)?$E&DFHZZe=$0POUPuEonxmq7>up-@>_!T+i>g^z6m>Mtls2({1Le zt0m^)NiOqCvH0wVQlJE}D>6u^e(*kc?u0Z z!pc~1n(orvjWOyh>9p2+Tu@~Jsr7VQ5Qo6KG8UXGI}3IWv)d8}!qIXNlBQnJ&}xlC zvQHV|9e|D?_)XQ@O~FGR6Ahf|q_Vp7dx2W-l|K_C}S|bS{O!)!)8g$ zx2Va?7uv$wCAe~gvWUJ&TJR=%jFVDe=4_(vp}Kx)=cm^uDC8Oe<;+xF&zG5$wB!)k z$8cdz_$cYQ@A_4zU3<`NfDrpmv~qM{Lmg^pca*TRx zT<>z#YU&qaN8sI{B@|`a@O9DWB8KR~4Ku2?6`#35%Xh~RsLq!5v(<(Z$z{sXZD8%v zuuGK#8xc#h^^F4!jJ%CdKGP|`vNEI!N1J9HeG_qf(hloguAPNj>dEIWKO9IhPxdWM zWU1*&sWBdmz17Vy2U#l5sAuF%BS)DQ+$g6(O}#3euw|dcpSdZx(?;Q=B>svi!YA1r zj{dG|o37k0rQXo+Gj%cG7)~h>K=GP8mvzgcPAkGe>|CA7F%Rag!F4LcZ3seI4m3y5 zp_2~3NHd00(d#iZzZV!+9`HkVSoY0fFI|P}+YHIEE~ppON9qg=)@#2}h0QKsx4UX^9fb-CC7NtyzEn6IffI;Jue4s)wnL0;uz}2|F=XO{i6+Cd#;>|;=O6Yn zfG37Z)6@Fe3uHfhCQq`d?+dP63PWF3l@2jSFKP`oxNe@>aC!xo@R2$8q~vjk_;LtD zKaGwR-k4(_6<1nj^8OSF%Kh%sZX-}20NlWvr=FVrJm_dE4fv`8L~=h-2$4M=LUM8? z(Nt{icdsiW{c_D1H;0w+Cur4W7_4T&VSh=yayogLZN#SF9^Z;k7Dq6hUUi07HM#j# zL`66?oC(-Utd)aEK=PKt^LQu)g#g$nDKP^Zu;8+4r|5~wUYKp;dN?Q3VfG9*drzTeN7)m5%)ia<4kh`H-%V|Fn+_tBTEY>_0kL0z%m}bS)~;ikGOjVfKUSA$+nm+%^}XeC+#|D&?M!$t8{h?{87L5!sSsaO zBgjRosISsh;0N!eX4h~`{y2wtRYhe+C`=dGNt^O6;@9ILUfe33$O@Lu)jJ_@$@L9J z4~njzM`EJcG<3)Ey%2s*ZS~XcGv?_wOWj+g3l2vG$>r8wFm}`RR`VfnM>6pj9FoG&Ja>KRy=8F(t6lh5Qxqv+DG`<(wYoL&i<@a8mU%VtsLxbh~Z$rsjb#(HC#w(q8UQ z{Uv;KS*8ObPwV^FnKx^iH%FA3XBThpEG9*9F3^TBULAvq5cK0PLdC;icW<>*)$36G z8r*?*j&z;sK+BBXKF14+!R&!eSoOd5V<*xlpjiGrnjEaFCNA*ZpB z??RWltzOQ+Ub5_UoNbry8s-jJDoiI~lkx=7FRxzDW`|QM2NnVTX#Wx~WXOv?%WLgz z{5ct|C<4nPH*q$AkARdJ z`W}&x8S`VgFWKXDr`$oOb7`hsEvaO?RXZx8xg<-aL9p8e9`|8@ftnXL#FjBLQGVuI zrjqqoqoAY=EWlg-*yNEIZ7AEGRTciqn(Dt*uM(XuZAkLSmZ_?LlQ)AfFznfcPx0MC zLG4g}AMYxKMk7{~W_$RX+vM?Vb?QT)M)q>5xp+xKijt0&HH8M=c=L46%OwC8I_GjA z#hNT&N2q6vl2b%!*vX;3y977D0z-%?BqnP~$E2kD{2E}?#*kxVfQPywiQtJ;#$Js6dc1i_&lWYJ&j>|52qz zeU8!1vp0m9o5NmLTmz^hjC!0DEg*|MP+Izp)rQ*ULCbTio~Ob~t%8za>=lK%Gwhq# zCL=|^Q{qTd@fRZ_krfTiq6Umj$bu{EdKWJY5io@Q4L;y ziFN*tIhL|x5cx8ThulRgb#^zee;Gl9(r~A*$~--Omwt=Ggf#uw6!ffTW-RXstp?lr z6&jFV4}0ZTBVT(D(k8jD<`H|zB23y^(R&hm!IyIlEO?i?v9+p)7WiumeP#z zQx;9Z9JEYlb17D#OfBmzXdL%gm?OLP1F(}{3l1hmU%D;>c0E8nh|+|=aeI`M(X#;p z^++Xbu&0$eW#N58pb;(?My~1U@;9K66atT+R_a@R9SVGTc;#jA%6|5x2CggzaeI*G z#5z0Gr|n_jJ>(7=iu*u`6l>+>J9K)94X~ zn1ag z^_{;UqeR5SI|ytM0Kj+Kv|~LpwoeBQ!s1;gY&yRiWHwpG5jb1YEZ&>IN^(vL*5VNz zOCcx#B_TQ#Ql8bG49!1kk*E2ZJN#(qSl6*ME|3 zBX9I}3G5BzoZn;n@3Pr9*y^b>+Zih&d0Y5q<7trm_jZp;R@uCESDX@j%yRzxVHwUv zCWpsYDns<|f`fgTjBF(Bp)YxUbrJYcU?^~0*8B^(6kJkL2bZsO8MPkfHK?cUj{@JH zVkHe44hQDRPxotaW3GZ3H>d3d!Rdn9eM0#mYXINRY%WZlM^%-Ew z?_KXHOXZ-Zjmf5seL^u}*{?TzT??|fIW}Wg<HjC5z&3-G5(JaeAHmIJ9Gn}6)awf2jWbX#bD%=r!Co=PH z#z{Td+-Z>pk7}+}@jvIpE_bi6xd~@KYMG}*+7}(yIK@rlyg8~^mYgV~;0uxF6K+Z; z@{-@{Uy&`C>UbA4=2h==+zd@D$%foY$o1)G#Rfe}!4SNQu=!SxVqSHOtQs7lFcK*B zi-*itNDFRBr>vTcz-0kn$DUGtKW}y{w6E%C56@r=uJ6ySZ;;$kVPsYOr#GR+8*Mxn z^K`#(yAuxs&kPM?h`|G`VU^|Ko+& z==!x;zNt=9KOMAmop1L&jfB+6zYc4tWZb46HcoXGYM{x0+aG)xj1ql_a$We4LqOUL zTT&MQ)8q5_;QS#w!jv%to7hJgLb8m$Iwq`2)^N2A<)TH($QL zGnSm(7`iM77C1?F5>9tlXbU%o z_QbmA@u_wg%p5vh&)na`W7ZQCNQ$6DgeI~~O;wpF{y_Sk(@2Q_ae+KjQ*}Hrds+W; z-o){g5Fr`mD*Y4h%1=?=;E|2|l!5`@B=>@IZ05*>Vph^<6QJdlaO1#UtI4;IZePRrXZpVmB!Anug$1`u8tLH5{a&pdj(>}NZV1l@K3`gd94 z)ASVw&wKo@hgIP=Q;L&FP zu8j6;5%z9?CXN6RuJv3D;yaVFa5IOJ*-l&4JB>_~>?!d)?jpmIdnZgR*gJ>Onwf6h zt;b%lmV7;g9Y8cB0ecd0?QjhrlR5bCaP&H@YtU!NO*Hq#qla*j|JL4_Mzfi=4_v3y zO5{;YSnsg6&ejd)+j40+ znwk9KY@onxfg@MB&`^GL2t+s^I&sqx^=5Njbx&_9tks6wm#iqKb#HtL4BtK4rUZrBcOY$HezaKToUJ@ zk$}#vit@#B5OZuAYL0OG>Nlh$-7$BIP%#YJx734)a&Ry_^dvk%rSmG~dNi1a4!a>- zP}Z^)aP1AFhh<~>d${GP`zrsFa`)jc5ZLny)Ez2bF$d+s_|#5ItG7HW6rYKEVW$5r zGZG&AHfnucqAgu}X@MKKA?Us9obGE#ijAUWv%OP3Sb*ynAbdGX_>r(HGtFm!>0Tpj zOOTVK#(l|Gs^$FZz~xhGoa6FAD9*CmJFEs0)pkH#g!U5?Zj{yRGaU3R^R8qLW3o#9I$u6V9($vbWKjQw&guN!B0K2BQV1m?V#ULM#Kwz9 z_2XI88AIv`Mo8HT0qm%SD)V7;Ans9+NP{Y{yVEq_;<=IT-iZ&0ebUpBYpnfHA}7=W zp;j(^J$Nu46nEcozA=90i6X$E7K+u4RQ^)-2mCGNqlT1{VZUrZkQwNa*yOT$pMxKW1pa9fcP6ARw`IWu7bX0}GG>}|8JZ9ywqA})Ilv9Pz+WnnbD zst1ekZ*3UUn8}RG>0NFs-aGEvtOQL z?+BwW(62vv;o|n~(0&s#Tfu*PQC~T6;T6D6*7662oh4yxgS={ltJ`t zxDh0-M?}W7dvk2KFv!G6+E0bB^ zpojib>bl!J2vBb6pO}(cP#!5xajnT(4Ou1vex)UGqGk_H9L{PB68CdqS?NV90sIJH zWcrVsZfS=@p1 zB6Lew^N_2ID1RZ)nN!IHVP?T7oT|pAyY?h>Z|+(ZL>%F#zTUNWGLAJGoMev#R%VIm z?mM?MSm{-bZlJ^G+*63h#0*Q+AXzO>GHv8zTxQg;Q-3qHg4ImQ z5x6{_NbPaznb5aRu6N%FK_KR#?uQe>`w4?vFdK$$|BkT8o1F4~a#IJIa8BT^^Lr>$ zzI|ij@tl3%^s!=Iq#G)DQSqG|*3*`z+_K7a#T4|%eFcAK@jjH?&|l4XkCY5arR3Ng z?tkY2lx=A4KvK@J;)`-PnjX$=DFf@Vw>_yr8U}?WrV84X9y!Lcl=|uiHW;k+We4OgYSO3b@t2X!X6=!VO1}WwAA_-6*>!T_4(bE%B)p8@zjXjN=!Hyev9zUI@}L;JshRB6Ki?Fw2nN+NSnw; z9x=BXq^C^~J~2?u;lD{q?X!2BbB&~NMs!g?{L60&ry)nrWBG*T5vW^@GnEmEE_A{j zdnLsPIC?TzSbG{B9Gp!Iqi1Jasd(g7z{`Weq$#cUC|*)XX8y4O%DFe7^u--~l9N6i zPjOFyV1$cHOJ0iBg;6(R@ZE8#BVuieSP#`D5=;AZBLo81K-k)H52W)^^8BPUqqOh$ zukeVfF)gvy$&7cVy|A}{ib350<%d2BQGEkRCX`0XhG2i?b3R%(l=d`NIz3o+m2Cp1 zHUTamCQIhNQ?whFgAL8oZ5fnGJgHIk8iI~0HdV|r=brw4MefAkzLa(o{zU?NRN&q< z0;*}*Fw`*fUKgxG?{(6C|AHehOG}^Hs<(|+^il#q0o?l?W)EJo_-^vs;=n-IQBT#A zyFJN&k?{XE>__w?XP5BO#urQwy(|Tn7NYQ$V}}hvu7^(^>bnJOMr{^V(=#4k9#T>A zXJb8pHfk~1$U6^JV6%R&Z% zub}sR&NG5XqU{mB@HmE$z8Mso9SK)N0;!D9^wZj<8^%MfKdHMdi?mlco`IodankAQ zd8E0}0qNALh(_{;RZbl@;>13QBO~p9`!#m6|91lWJX{xmIU#Jm+0f|hVmBgyZyIC6bhnXSLrH7?(KmLJ#i#{ic5revTX7!@W+Q1E- z9?3ENm?6n4N*g&Gm-@jOR4<0`f9$Y7a_hCQT62Wp1$=D5k~^48x^$y%w1SQZEUDwY zv9SZ*AYN$;aB54MJPC4JZ3wX*Tam$n`B}@vG|Y$Ic3^6HB96RgH9-iBwY5m+xKSv8 z&YGdIC5jU1X1j<>+PDH7=T0OxAlBt~@T3eqnYUh(*l&g-DW9??tZVFkA=g6C*m~DU z_PBcNlyzI=Lh8rgBU*`JeJT6{vHR)G)ux{# zbON!d%I0UYsU*#5s6Dtws%=|QJ~(!a;02f5=lDS~7$hn0xOTn!ST@+nYq6yY>X8Qd z5+g-4C)doW<9RYihkXsHcsgB|Imgj19|^~DO@A&6D-ufja{Q6F3IcXxLpAv1e7K+HX3o$uAnxfq zyYMih&j8J#=k3(Ip~zoT`?H6~U*hfx_w<4dg{%>Be^_yo#Sk+mB}bb6;57sf8k$Va zVFTnMT}i{Q9l>DE!h}QMivi1ucwKzDkIOQwJdoGTFdt5#Bh0ZNSuUd|wruOwilymb z!`gxu{}|n{Y^iV1ePOgm4GVx1JF+D`Lx!Z>6|R?G`6-#~W-&pJTW}6WZlq7-SCf27 zDY3E)Y%yjUv--OD7d)QEkvrT0qF*iFJn&B3dnupc-b*dXz+#u%g8jYP7T&2DJ)dsH z(`?!HrCa`_;ki&8*r>P(t87NE0T4nQ`39xY z4fEjI7uDHHd~Vs7&^ad?ckvQDP=p)yszT86TrLrO42eze&wq=c6T1>IAUMN){nFyA z>HNHW=9M*MEQ2bL!|0}-H-I|5Q;W_D8yW;vux&%!A566zwSnrLL3dg6JWdLZ)Uf=A zYWS7q0ppPTG1F@*586kGnB?T?*34VVkCpVG;`k_TP-#|H?#;lZr*`Cvvaf{6UapL=i4&JD~95@AhsVZ@3&b z-OQ7C(nP8kihPZ?Wdd@Kuh5W;FP=NIpfnU1PB-UL8L~f4N5ti0&GZwfjcc)yNjawF z+Eg7@f;E>BTc}zl2YHbQYtsEe7Z{T5=3%wqHZ$*6GwKWVxzevjIwev1SnF8_8EZD~ z{A1qP^()h%-?X~lV_)FWn5v$X;bC03R;ay-g?!G5joSP=f9>y&r3S|S`X)PUP#`IT zZ~eOr{@8p>@>9mdkE}X3f)Ds5zs%Cc&CJq!-FBk>b^E*VU=&zx1u+Ml&2rUEdKOC+ z1#TTM{?(y~De~i|*DoD(~YI6xi4)9hwY)zm#p3+MAD?`mJcYvzzD0a#L^c71Eqi9}C%xHHnht zjj7ApOfeA-2Pojkcdz#~l%=BlvD%5}y2Cr=T#-M40ex>%9)g^TW3Gh!eW>gRXz%$#njya0y0PbvePKUgom zs$NGdIFB-6phmO190YACFXq!4LzmxkXxWI~E7bDnON_PFFxTGGBYyA>8{|=gK}*Q9 z?37v|nBh$QfRr;Cf4O~)7n!W3N-4*a4=J89VExj;!lh%&;7)D+6K6M!N9~~053u36 z+2C%X?lhvuNxGn4@>&2BEv2u8gx}3nM^6az)3purE78aJ8b&B^a3~?0D>Aypp7`G5 zlvZ{OEi0hpcs#(aB{~ld$=wQr;Qmk&eDOBiQOf*NYINb=)Spt~>N`|5^7V59rmc%w z4Vto@@9TqtjBRO!gyoi^{?M3idG;*aPWOJ*J8Utvs0@sCb71Dl0sv^?RFvhdA_vW# zJV3l$gPq(+hhB?gyjW%JDKFt($yXqG$&Ku<&Aw8*a6r&KLY4L@_db~Nd>TPC;DC3u zwtqz3q*^aK&>LDdos7!v<@pbPry2+boU&!Y?%Yq^)-qJIf3)%xmBczH zzp(cNR--j@u3)hIqPd6a9@#Fe8e5G+m9+l;hS>UJVCt-?tSs1U;JR(XC#r|wTo)}| zbb&qq%CXK9B>_nUifTJAki)d~i#qmamEpisTBa zuuic7l6lJ1DCcMboHmNO=wol4)pHkaWaQ+RaAcN!N#}3e3wKlf1p@o@TyY_ugAQu6 z;_KQ&u>TZNEl({u(0uP zI!m42oJ=*Dm;>!Esw^R;R)1^9T}*(Xzh;Nr*NXb-!jw;~+mzL^^N6Xid_VuyuK}iB?{3I0fn5T-1a=AR X64)iMOJJA4E`ePF|1Sa;{~Z1omUp^4 literal 0 HcmV?d00001 diff --git a/docs/architecture/assets/IBC_relayer.jpeg b/docs/architecture/assets/IBC_relayer.jpeg index f90c89a74bf11824b14c0b0eb3b1bb1d0e9d9fee..9c470d3555645e972f260c18294261c73340a665 100644 GIT binary patch literal 121564 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

Y%=stj95?m)*RkiR1BU|&XE@Fx2TR{74 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

Ck_{W=GqO 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%0A@uy-e)_p$_ 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%0A9##wt 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 literal 254871 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ðhS3e z-=qHqwoI-j_zp2^$_VXtOC!Ri`d51T+j7h0Vk&QiY8MY#FohWWBMry+h~5*a*N8h4 zCFX|EmmxYM=j%Y0_RrqhNYf-6-_N#6AYW~Yx@Y4i&83C1NUw~>4ISk{ry*b2PHW6T z1_LSDFYhA$(u057w7+fQ{8dEx0o9>(*$&moWnwd1wF6MmO(UQ^p)=|(^5X^lq$k6jWAX8D(0e2k>C zZ;aXX-oNGLU%K+iSn7wkjgDCtZzQG1!A18S{zmAHliLpmw@;JtP6Xkgl)Pva@&F(o z%=@W~Y0RrVFi2c$A7}b+0)qfI-_S`&L1U>KK-zqw59oc+e-U>TIpRjHpQ_CYH(<7U9VTI zgW6~n=Y*w9Rpx$Ox4y*rGVpCx-uv0fJ=>Lv8CrOXq}0+6grZdb^u*%|gAVbZ-&?jGFwVqlU_4K6UyWh(eSGe9kR2L1dFCVFX{z9Fl+u}RPy1aR z_gFqiBk!ZMr|?=Pp}$eX-=3ELtqS&6-x&QL!!rsOT3C??Dk52hhj55^NluDqGUL*MGb*MC3$em{KRJSr+!7F%bQsk;*D+nW2XVR(8PsX|3@MUd8CY404)yz?g{ zG}~T6`Rh|_z5PpC_dk~!txXHO8UP)>bZ7Y5jP9&>3fPWpsSMj%(Gpq%_pRsu#p71>sb`D4N~vg|449A;GxdQq9*#O~f%&x{sKz zYxG(L(X#+`+3S-;dj=kWxrHTk(boMH+_Yvqt0pAJ%!8^2teS@2SE)U-l>!e#J4BKp zDzKSpi)&dy9tc}2JC_&b_G-(x32SELSscGRuPY&h*kRX z#T=S*L1Ceb84DO@4Nv9@NCUOrJN3&2QxL9X)M&h8pW%)eGz8on+AvR*g~D{-fZ>^h zsVV2J`M%1klA3lMm)Z~-MA#;$r#f9jj)w~_gOpHDee;mbc`%nL3oN8AifxGxGu+IpCJL8z$k%JBTp#^pP~5B!ExpmxevI%BxoXy!p?$p zq58r_{`kywEZ4*31g(7F`^&Ww_I=HZ?z?K&8`Q?%o;Tg`c`Zk zE+Hdbk4c}BscPHRH$CZuq}W)-;}dmFrnwZRZjQ3cZ%0-yb?WZg67?_Zv%i;=naQiG zpGEp30i`^;Q;)QLWZ}cxjpYM(bYWWf6b_ORD=AYVkG*Jk%ZBjgkBjkp9L8w=CDsIv z|LX*PD=-r|$IJ1!vFzyRLDFE+cKXlQsxeRO^OP5kqQPY%Tpc4KGe0#20S@&|{due% zjq!ANVypRuGFdh7i;+@ZF&Xp*zLY(YU0FW0f}pHgP*uZXSY*XW!nE zB7SMSG^W!zm*H(cJr<+=bk3Y!M)u-#FZaX&R?8rhu2?6c0PGY)5BLc*0bkn;SUAE( z?!BI^{qhT^$pl?IT`&&1Ve{r)kuy#33DBWyKWc)qOCMER!0s5Ex)vp#T`)a$#^rQ` zu<9yOB(tGA$M06qlbIxyyllZbF!F#iU@)SmqcbRCRP8pBUrN*__wg%T26EksQW6#r z^ycmb)64#&rl*rC-gpk4hxdsInBCWB`Cb`! z76|Vq=vaD*r8lk+y!hGylUpb4)JaL1xX-g43;DJ!+2u(Lgbfpk?0#@%@~~a#q6Y+cGOC6^ttv7y?LOC4qj;jnU8uXX?;r z!5b{#2dq@Ik@?82v^vj7ni*p?BAriq1tfj)MOHq_$+y8qUf37^UY55yQ;p?pZeCb= zYK&}^A0i}u=DC3_xT7Lr7x9eIvwL&v#}hlK5?UNt)6ce;si@3XMQ=YvD#;FTatCaA zAt)wA7vc`m6e1=%E4xEnTuF7NqqkgwszQx2gTulY;xx zoUiN1J4-GGH28f&79N`eQ^*a5IdH^B50rXlk(fH%#9Syd*|@{p0>wo&6_%h2I}6<2 ztQP2Oy?3#S9+1h5SnFk@+ZfwQCTm>_N<6L6boI7Y=MA)h=qW3LlOcZz?M!zuM$9P zAVpx)r35sTkc0#&0YWGW0@7Zq|O?oygaG5v~WT>g&aJuF;0RQIvk8@I5X{}Wt5o! zJq>^ZcweXtM?^bA7>dVdV8HEw(GuJ=n{IW|dSKW)YF(ok3Xo?@bm=rK0_IS@g$WD+i?TIh{mwNdS}77?{G>KS_=JI1lk3HRU3bo_I#;SN@lNHM zhgHfyo%cr(nKS+QG4pL)xnXrYr>m+N$Ry}HOqXR*t9ZZ@Q>2(^e*a-uHcJ6C(JHf*F^wGg;Q)R=H`b>WG4tsDbVE|+swJ=GHt z)N!KcBVl!tG;A*?yRQ~p%+^{v#H8q4lI>;~ea=yQjV5Jgj&%Emcogakn54yz_(wo} zE0!y{58|KT+}(J78(%n14Q?F);V!yivC{Fe1w3fEx*O1*F)=UOUTMFvy%Ug>Eb*DYBIP>OXAL;7xkrwpG57k$;KpeD( zoM&PNZEF3ZBB9La7L>UCQ z(s5Yp>w4+GZ{-w8RLm@D1!j>;qQz742|514BiP&;?WYAn!z;>Mf!6CeC7DH$$~n%x zcPYE29M3uFOki{Mr-gieTZPQ!lU+tY3Qy39>I*Xgph@mqzIf}Mg(r1;EawJ`Mm`0- zh6FtOno54@r_m+IGWo(md$FVyuYZ0dJ9@MH>hz4SJmUNgocc~-_jV}$@JBEE7f<}2=YQ4z3`)lM)NM-bC4*=I#|(X}f_m1S z!io8c(4Ui{QAftdGv3CueBodODbo0D2FyR_BPB(oeR#`b0pJxZpK;q-SWaf`U1(x0u0(#8+gE!qm;Yd4%cC~96{HbCUL z*Kq56$l~6$WK3;fbMuXeZ4nI*^nAnP!ym2`LyJrKG@&3!E!a@VHnwKPACbP#;J|+r z9C+~JsN`e?Qt(oi0_QHE75JiQrr$)T^ zvDi?$nF@}88_~W%I=BTuD~n~_HPnIGRNwzAga2`E*Y{4H*zdeOpMIr0`)%v)+a}6i z|8?KPTAcW}JJ$8l3A!Kj0o#9)tf%gOyrR}^k!`faeXal6ZxyqBZ~yg}to^uq10%6L zglR-xe6xI6-iOlI7+5*kg(on8&0}NKaQ&vC)-gW`At(J21eq^1EW`Bp?LSTPH^A!~ z8(A}HoB9vOlQkYJc*W2?Ycx$@4ZO2vZBdqi#B@;2bj?yI6EYnhY7m1v^?`+Vr9LFC3D9Ku^Cv+wt!| z_kbAQ{xX~LJzzDrC~Uyhij13vT@!3gla6e$mh%|yFC1ya6z|mp;ZH@4^aDORf*EzL zs2GtlXhHZ$g0cwg2*k-Na^cp*NYGZ>ZN+Q0WO##Hply#?kCGXg zv6|vID-dxElX-6|;V^v|VsJ!?MyLRhR{I3pUAp;HF)Zz!I)9T7qr+p~G0C4E!{UwZ!^n)v;}uFnu48;Xtn z_~IbLxE0q8jUX!+uL=~i*m#Z#SGnhC2W=Da~CD?B1a7l(k(`0 z%^sz;c3=*B0RVb{_?L9B&cL1I$XI1QsuqAfSTdiV`S)%8zTN*m{WZX^;ny9^p4K(c zoMF^IcE3a4&}r8#zk?fdz+V0Pa{u^--TnR`UHleG*l_K)FWG>G`HIimue|<*J#zdT zBA{C;H1w4oP77cf46JpNR2Gtjtg=NJ zic@fLnk0TJN48IC1*hXFx%e37uql=-nFN{1f~)2oGqugeL0u*At4r3&+r1xEyFb20 zvK7Np4WmsvWvV9f9&Q_CXt?grz6r+68wBqdd3oZF9WwaS2>;tpzgbD_3v4>s_RVcg z=9$WRid=Q>3ZHTBMGK%))wX=VO7XT!*zhDGCV$2wKX30Qo|TAXk#uueDN%_zc5h;R z5rpZ+4MfeHzRziA-FwZsIm5$&PF+3f>Rm&?twe?FziYvNIC1QX%BzHvx?EsGxlBu1 zHSNlr)xx?PvGw)cPsfAL;Ja5BG(n@z2^XQmnzb-~I*j`IS_{WLfvJMO3LXpl%xRc-~UgXW}92BzeeYfA~_X|FynnNfu zb%|FjXN?$?R}L0;$cevQU#fTOL5K&trSX`@JV zVDErUza(7up3A!)SF`-DJ_}DVtF!8s%YlT5PNZ*7=W8J&qAl;AetDO*aKd;>QHN2E zwo`WNGn6t3uY3aq3$$A2h$eJ20;_A^RbF>= zYb_ckF(@Qq!Cr5&0TqQtghnQmP0c^c#b=P4R(J`>P$|bd1YAtvYk7V)pJBK%Sr3bQ z0J^sFTdM*jC4bHev%{xfwsKXS@BrXvbI-aV0fYLTiuFdo8&lhh|n< z$*B|mvd6gB8haS3w_`EG!vQ3yWGQ?E8Kty)#h{f9^fT~nmx%Op%qf%HmH1e~=b>Jo zCblC)vpVlc|2bxVUvmN&SLD1eqsZd6RiWak-M(GPf=szL5}6w4kOj`a3ofweTT=) zW^A_Lxy7zCj|j3lw@BLa>vy4kbfL(%86T%tPBZjN9VAJ^v$f2jMNq#-AlZrHW& zhf`8Mh21dw64k(+L68(R(-zG0g(EI)=h63b#2&8P{P=QLL)eHE?~Fo>bokJiMMW#} zE`ku6&a?YM@e{whgU%7!MRG>BiAFE*wfz0D7V2~gp829fL-vj$n#~EiN+vsxqG4tAOU<;8WzAtZA(mh#y>gkzCU36Z?E5(7esKQQGNao0aNO z7xlCq*O?Qfv$*qJDMrI~5#Gk>B9j86`7w7ZM`#IwPUralu?hct#A#;XXhKPhCKXrm zB^-0TpetNeX!n`^LFl9GgC+<619Z6_21)_bfM#p7(yeXM1RJGZPQA~N4m!SD8Q)=Q zfbPrlXkKmeYJ&Ul|@{9m7M2h-cHZ zv%WTB&*%7s2iqJwwyMvAuCdPs9cM<_jx#nE3Z1>>=#yq|v+&jNQ!eUC;XMLb0ditKoj^Dx zgvJ7v)%fU8J*;iOK|{T*m_Cc`Ew$$#yCJo(Jz}_o*f%T9Y<+GD_*E|Ypq{~I?FPpo zc4IiMc=z$CdN~K$LKNma7BaKZ03QD|4b3efJ0T_6J6o3uM`a*Ob<%x@P2MN-bgFex zxMtE=5$RW^%au)~onxZdjGR%Z7?1FXmaG;%AsOy*%iBRbhWpgU2cZxa9L@5xck$#F z1$tO#Jmlh%ACq^8>Uw)WNDk9;h;4{1Xw9h&-MW%0+mP6*6s%^>a>)hwq{dVmcQ+UJ zRi0E0gB;Uy942gQ{KM<75T5@;j`&@C7tiu{5d{1G5?A*RrGLd?{HuP|UoF3`fQDwo z@&W~*J?#WL-xp3KSgF8g7mxe?*VK=Bn$&p?MIyzZY#E6A_g+7+EA0LevRVWXe9=u< z$kHc8B8Wrv>+1L0u-Kg2$7g=>!9eY*hRBMW&aSdBr=%ESbgaLh(qwBrA~d3oEw(hJ zUczZ;-?Id1w$Fnnf^}XtRm2UHuguKH$j`d%QT+WfjtX`|P4h~1_|3gt#mz0|84jnp zC;UIXzl^-dT~lUoWC)~7aH5o_=Nr5>tM%xjn?Eia-uu{>NdY$GzdG7~AOt&oT^lS20D4~m{Ae8)t_|-~mwH?y zIq6ZsvCi@Hd^w@jk2WL&0nk?0)?IqKtHkTq-0MLCVHN2on%oUoHUQFJEJgK$oU0xy zwOemp`R@7kKlWSv`DflNDSD1{_8zhH!d^|OO4y=UpVRa7Yzf=cZowy5lGzTdqE>fp za`CuvfvI4?si;B>ac6$}Fh-SJ5AHMY0=s&`*NBMFTz2ppQ@QWDQ(MydVH!>{55;$HR8w%p^4I;HNG-oiqU^KzlnCuZAS2V$uuzr9b}-?m@QG^lnnguX zNCWx4bCUi`v++s@!|9}M&T}H0wLP%+lGL-Ii@f1Ngt5E!r#>X>ixv|#^QaJbs-dYX z4Lp4>yhN>$hdV<6EnmlbptvcGa|vUb-S62K-RzFdd=P&9q=9 zCZ?K>`rvQ?k#a?BcW<;4k6cN1tgFVfq-4I5yv5u{vYN;q|7z!}%{i-xMAcNWFjMV5 zbN9!_w2Y$~)3A#_g$@6dpo+{cfn7_Al(|=O+h9Ty3hiD%vqrmLL)|;9WR3e>%)yC? zZJ~J&!zc&odXp_EdR6z!s)9o2rc;vx`i+Z7DQx0_0{@663Uw{5s{f365t2nyd>VVi zSq8bfZTgL*HBC)&cgw?%7&qVT4&MyMPCMD|6Om-4d`gJKDDnEuK|j7t zr9yW+_>5^WU8?}N#k#Aqql5>st%A>m_j9652$Wi>c)~Sr2Bgl~Pb<7@LoeNI>mchv z)BT3ET_d-Q4#LAQBT=@RlPaJu98F+*3X-4GfG&Gd2PNV*g2%_|M1YWUtagL+%*&%2}b( zt0&oBhT|uL@wo$4Wf9j94!LI-pSs|v1ZiZW=*OlsGAtV7A><|N%^NaZa*AKENjA?1 z%vHYvStv1XrIJz4F<%w4 znbcWDhZyx6Cj$S=@k;;f%l-a|orB{a7_)^pp0J*G-OJYA1OVAXI{S&1X|kIB3#pjq zT|`x8GnB8!mbT?T;EjO2niQuUr+-}b<`Y|<41e$H*@cPCcgh3KzhCL6*7)*1*`HV_ zS|Z40s^u1K=~!hX70j2F;P|6^tzWhVv&PNC3wI{?t=aYLxkU>QigpBbJfDW&dSiHNt;_pr-_W$PMJ_!K(j)1^JYp_f z=_rC=_a9U_JRhkdZg{(wAz{q?86PGdzQ4YRk~##VxQJMNB)q3I5tbBb_7HP6*>qLE zl%ptefkqk`xV$n+9yZ=UTx%a(JMbcRFgzU1m|W;}Fc#dS&V_Zz&{{8=Jj*Mnj#e^+c~$B4Ha34M!D<{)BWoYn=ZpM2k52Nv63YQuM|aa{y_bvkT4qEySM|O+-nN&*_QlSaxFSjA|*c+%%F2)gmRsT^YW7WW87Fi zT@=YlEQ#7l^-FD1)D+JnwQto|&Br-&-vWqq!LyN8(US2@;pZcktRZq_ptsVIs8Izq zeouXnnMGw2+SawkB?V(J?9HvGr@7l{>mAMbB@djiNbzQn~pOL%@-jh{kmRm^^( ziz1?pU^)}wDkQecy!kb4ehT98K}+dSYpsATsL^-Oj*RPrdram{!eD(+U}Hf$vRqM7 z^Fyt_G0r(&V}b03gMWQAVL#8KV2n!VJPAerJLp?aF)GY?1jV*ylJ~3`zXx%w{b=#9 zBB}UUv2B5aghg}}y=X??;L0P|uOvJR;yRyx_~_(s@8Ov0mDF4-P)ZB8Z4~Fd&PVoz zeQrZYRocuTf_J~R%jz!SQw1JzH`M-7b7YoAMtBh>VKoHXKmMgZ8nY|cS#GBsW#5Nf zQ!~3g<9CpUH=NCn%%dhvtS;P}uOn`wUo|A#z9Cc6MVR5mRN@>@swSH=)fB+b8x39; zv)~H55h8Yy7Yp;6<7={7sae*Su`f0@|((J95V`D7ak!|w@pWyiKZp)rIO zaT8KyzM#XWKY8*C^F(QQBG~$0@w@#CBDE%;*hBOZHtEKbaOD7r9{swlV5EI{ILN?f zV7NZXjt1?4+c9B0g5`*1kDgnRNGTXv6n2qIp1-f}wXIK87pDh>fo+Qd)?j-Z;w_yx zf8p!15#%3*rhe>ayS9CNR;xvCUmLhi`gWxCT^`Q(|NJKrG$z;J0qjb~62+2m ziA#U~ncI&rnH}fMlUAJ`Gr|!)KHPtVw zWck}?tgmHmP=g$g9I0T%3hrHNE#_+!9&b>zXNSd2yuRYPb0y<-^B0cEhON?d)~p5$ z$H>APLyA-fTOla7;Yp~klH92k#?>@$9blhPChr#xo;w~&+d!lS*;Ez8T63Klk!FFq|b%o+*#>{lcLX*{_;sWB`u%t~nwB8-iuCv8i>H`X7vfXy|?Pw3mRr zGfGIc8O8Wm_DnRQyU%30QwR4UV`Arn;;m!myfw(bF2~bS%dqkOy_qE@nbt%WC@Scr zW)DnxBvyRZVsEoyXPpa!c;P(omQ`+Y9uP66B=}f9rAuFiXEk;R0|Uc*>{d>&!XuIH znpED0LhJJBS7XQl-X-t`zeuf+D*+`rQiH{YU^Q39zL57u3_Hlx(-yS6Nz?N1%9bUK z)%Jx#y26@NK+jyve11C5gfF~8u56@T&N_VK&3ibkYe*zBF{M?F+NuSZmA-IRB8>Lb z*PXrls5oy*GcAJaFf$|R7eg@3Vk}oN5RY5MwrHRl3aQuNm~jM6WF(O{-}s@6Pv(V> zX>*nok{!tgFe-G0n20;^o_RW=t*~&w9*!npHXe@`XwW@yxQ_H#eCWW+iKUrOv&siQ z+vO3PT*TJ6U?F~uYpGNZ-vKg@F_Ro8sk}*YahtS|h17fsEMU&YiB1U@`=81Av}7da z-(F6&rI$p^5jh1l?KBsq4Uv@o5B3?oKdD}$#|;RFk6xQ_wwWLGde&6mGInVU3S22* z@&!5rHOk3}i>?LqP7zn@EUOy}18<2H72MhE!0dHH;|dMCsvK_v<$X?))E&+Bvpv}b zOU%>OlO(3ua-kUyJKlScu2qCeU|=Xox|!qn1o^TxX|l%5hzIrZr{j}HRd8CYgASRt z`hkbvOGlFZc_P}QCnN(t@jSMmAD{XNdP|3}WnhoP{9Rrlvfr%gUxc*wEJ~UpU$a)rmo~H-(EcHj|ydaD+y` z8*%QY>$z{eO!b;*zUqI|>7~k0tGdpne?qZDoeBMOlaf?>2I5W6Jg-4lkMsqj&o60& znM@c!?Oyy!;m?w+QSAbjO=KG?2dP28)S)NUynu#?*GyD4puAHWl{1|m&@>TiW4Eka z-Dp6+%ahM1CgZm_s%x2mY;`o(aweWkt=q?nBRfd)EBLX;Z64mT+=|-`b$QZ-Lo6Al z5h*<*ep2Zf3FRwa*^nA?qD%1}qe%@4d4nb7yue$pn~J#9x=~%AU<~(#*=OOZUNG|< zNJo$Doy3yNc%&+SE6}r`miB;&;w{!&3uV|R$6<@OM(lO=`fGF-LQ60d4d$k2+^(lCarP$>u_JLYI_4 zLvj&OP9H3;o)NG5^p>TlZwMA~4w%ESwR^BqaV1DMi+e(4?(M+$=;TfpO_SMdN?Bx8 z`4Qs=v%SVm8&tcpW?_2x3bYwSonDF26MLSpL38*84qu(s@jEH1VL7oB6L91;xcBDI z0|_!J%woKWkZG!Xr--rGvKTimtxwTi$QXrYB-yrk{n!+aZ;)JD+LpA0l9nwB-tXAP zGu9fSk}#`=pBSt0aI5`;FLpC5E`v720OIN@vU5KUMR_1*tk;iGJe0 zwv_XFf5*x^(5O4xY#>3yDOQtYoe-dQCgolC$_?!N87jylLyN6K?=(eUJ<)@08dc!W0)sE&ASFYaq%Rq3#gwP0|iO(a4obMYt$}R_8{_$ zv-cpATy#Pb>HIWDOjA?XR^msY4OGLX*ctLX?rc%7DK@Va;hq(_afE%%(|$wlQOX|n zWm!uog@%*88@Mf4^4%)aeB*NKZKwococ!V;S+vDdbAX>IX_JU_7|sIB@#@$ZclXFj zPL(Eml*Lq4tCKOcRs)fP1jFu%^ujaxd9UatgRHBSamry#Wx49Y z5EE2qx_f)rkWzb4SpCzODS)JatJ6~ygDoq;4&pZ);WQY)ORq)B*m*N%wQm#|mBzvG zseIMmBrR*HhSP@5$F*!Y1xNLNpW@$Byd9ss(DgPJpZd2`d~m7+fc#cPsfwsH&5rxY zoqLq&&nmj-e|FfBh-7mjL=d-h79s^5&eOJncix8Hx4XWtM1uZsUGL-C#c3jau26DF~ROT_0U(Bd~S~Wv#Egbdw2Jb~w60K88bZr(8 zF2+UP)rrYS@tj--FUoD>RagU9_X_lfTgf&BYG?YyP1v-B`*siKE_!XC42EO@mnZP& zy;2EfqrSbR-7rZZw7s&~{JM;&Yj-Lr*Q>DA)ICvWZDDD`2C9JwQvMOx^r5-mCGa*p z@suM8ZfhHphmVmY`YANWim@?f5swk%<@XorLfIm_eZ_#qn^4DR&c1i=cuU^Yi`Z*c zpd6aU7GkfM0-^$cmW|PA%(N+}Jj>7cW0NaaL-J&)Q|fgp{Hz&3F|J6}$Amc?Rfbju z@xVSe^+`%%vBG!V5vj&+D6(;z>bUIWO}bBmcUCB|&Lr$vpJ3}qUpF#k=XG`e0$)v^ zijXn5w=^C)?Q>EyznEkZ^@}3jNB+6FMlth_rr70Mj4YS(QEeoK*OO`{ zIeUlD?c8p%*hG|qDFZd%bLvTE#z~|$#!o}xcb1$A%I!6xk#Bo|o?Lga7u^Wy0 zpV(N0ndz?T-{bRi9+-MwfIO_5IcIEVcG}Rmz@YcW+t0A~?LKKcDN6vE7__LfEFJL4 zvBj<#IaNBhZfOi&6zTa0X+Fam^G*GcVBL33u{oz-X32u!tawDU`!=*YsA~39LW>LU zkvcA@TZR17yLvgX3}~h?f*_-h%VS6Gf2;Qe;WGdfV{A>PTB$=PNlJC{5tDm<>19zvyn)8DkXSGTWCz}~HDq%encwni? z`96MqXJU6%j+SKDcB9F^4+Dt;+ zLs*i)TvzjUWxgYn&0OLoIw@VmH|yGK7+=?5JVxetOaWMn7ntn+BFn$1e_0yv&8$X$P2?Wc!tctkU|O3wvcW+7`9sGCeR7?6vKMA_j#s z1cywD5Cz0uc7^mbiURA-TdCLu9-+X*ovH%Uwp1;5pH+*x#t4izVbFmiHqp=q-E8#E z)AzlAz)PzJ$Wbu&DtK2R(mc1*6tJ7Q=}o9wy^8{Mfjf&XoN+6Ij#F!}AAlO|JU6^4 zGWJ^d+`M7~Y<>xX>Eh(o1NgA)Aq*ux!QG(1LV~mueFP)w86ZjcX-ojsB(dd7(f@lA}$GP!Um>LXE0L zyiM|J!YB^`gL0N>lwpH%dwWWk#x{d6x~Ad{91mYHpEQ^{6$Ppzxg3)T%Si-Bjs7g|F1r2>&Bk(jg26=GbgT!B1XmlZYF*py;^yl0Mb zZKj!-UIdUyrHW3% zoyo(V=D`+&zV2?}6>Z>qgGV`2ZFzfDFND5FOZu8g@u=TEqhA04!U|F=^^R-R$zRB_ zj!SMeeGjp881NE*v5>MVne;Bj03ec|iboz$#mKDaucj*8%6M`3>c0STIoPD8e~=rp z!GXU~_xxpC{8x_>VBR{L2?%*cD1r)!OAgQ2K$_|#{rYfO727>fgSa~!H%o-^k|f{5 z^BE)Ev4pHxEhEZoOK!DBq-v$C0ZS?=b>Evu^Mwsi++uI#bJMCPpc5^&Mp(B|*EG+9 zw(!tA%l#~`L_yVn?4A_#!=+o**NI^wn$wbXQ=YJzJ|XO<+egd3Hs)X`K3!8gBkoVF6U*156 zu7xhJu$gk{PF1dk_$27-{*{bI~a&Jl_i1aStkBcI=$gqpBR7qk>vWA9%MWAT+%t|L%iK@uF|Zl#$B2n$@{`2l-QBX7LJ(&!Nl6L7tpgH zR*(qUXWDUF{e&v%d8+qv&n@aWAvelJO=N~WNa#rHHV0Xks}hdIE;CLi_75WKS)`Fp z!SaRE3rja;DKvBMX}cs9es5_C)JTQL2BdV+N7RbU-30`rIdoUgBv406xUXB|ay$@Y zrJd}pc{LCjR^RJ-$+M+Q3q2F$RlM;8GPg60lSQMk?1gR5izuuuMCXaLi`_B+o?2GR z);M>+@a~XLjf*~hU{J;t*#Ji26VpwGUpH1v#_)hKmn*^D$c#w!JLTQ0*H~sPJ=_h> zxzBHn5j=ltp#9(mTv#Ko#jiDIE%Ny~T9_11=%uj58eLXU!X_kNjl4E4m?SZ3VSTv_ z^eUWG4W4Va7`^C&QBzxPxQo`CVPaNprIVrcTs(KBMmyPTx5~gv=(TRLcX4k7MKe7% zDlwKM{BWcpt4P@V$yG<5%R)XR4Fj+DOwy-avV*{okAX!4U9B#$+GYmD1wzHM;DoiI7KOR|uppmr*SkHWn#!eT{Znt4!-yVNmyB9qm zASbz-!N?sHQnQ%*!clqevd|=#`>^I04)M^tiV-O9IQ*2q8C_sQN9VEyzJ=^mAe3Jk z=iQP1q#V^luzcv4KVRAr@_8=k$>oF}{lpZt=;VLc7>j0|B|OsJND!Z+pJbPMV6s(92+Kwm>;tM_J9w~s3j*_ zmXK*>D}-F~MXh$q>fS5GToyCCKCr+>5>G|OC~9^M_Ybvdth-yxKC4EIc45@$qqlab z{Un(-QXlEU?Zng~MD#=o;cfZ^6IuGyyIKDp5fV zETgi~3|8f56ykFu6W(GZsyrZ5Y|LXY)X=57|nX!0xa-PIp1?PQ|%+#vT) zJt|Xc4-C0%QK|4zn@@ZVK0n%lN-#Gl*Rh3x`-bs!zm(YeDO^d*PV6g=N3A`>-1gcM z-jJ7>Fquh-&NhQ-r)a|-)kCv~MKnrL1B2z21lQJ++_*NXn+3-Y;!Z`VA1aM9%&DEF z5x(RE{dQ70e#0<7UcXpA9CZqTZ@~nLpLLP@!14IA$zW1|*UrM|4}M=$G>-E}vPy;^Ep`P*CyNx!xzgq}QgB~SfO&7R8;nVH6I_%plf^*Jg1^-f?(TWrjXQ@5^r_%rBa90~G zsIa>eIViJXV!?u!ci#^s~+0s_R@be6J9)HIR0#iPu z&zG78Bn@DkBCX2dhfc%JsBjU>7e5P3u134Om9@~HYwB88J_$FL5eOX#j`U@3yoil& zyt@<7UM~KH!)R;r3TP^@Q+*?8`oeg^?Y+d7O>EGrY#X@ljQue87Y_VAkMs*r4xNhs zD7zkM^H`T64I>&TH>8XqJ^SyPy+;6?s8& zSjuIBtZz*&v@l3bUeDBCU_td|dx%^gSfvXJE#{xEiClsBJOLU{EtKEtY=^YFm%Xb> z*DGmPz(8NvQ4zu>1W|i&0Udl|kpzJ_2Gtnrd~{E$AKodLh&_;*^4WUWqq4b< z8^g`HvYEDZv>{=TFt6TtwJ^xkV{oVhd}^rP?_-b!r**wsCC2ov3N7tiWfOo5h)Q^QA9YVPo4TWPo}ksF(DRC!!8I zl;p$mE-|ZsKiZJrd?YfSeFTGn5b7K9l_K#Y+BC%?D zP%EitlkXOs+#8fX_r$>Zy*J6ro!a2tEwS~J2K2_qdiDZ5%5hycY9AYN7-K&!DXe6hlS)sA|3)zKCm)^a0mBhhx|f(|K%;r1TxH6&;Z=q_>TDZx!+ipd<$ac<4);a3RU zYrGN4!k<3jC=glGOr<5b1RSiY-W~ZAZT>1W>{Zq>O#}624SpG2+%o7N#Y}8>g~o5C zXXKU`JcU&bl~dp`IXrF?4c@#_=-YMIb$teQveL8Te}saR$yRFese(G? z4>pRy`XEr2^U~|Rv2Ww*3B8vPxoN58Ka75O0FQW;=V2#Qkk6o$#mGC3E;y|8YLioC zA2O!$phr%c#0r%j)3W9APo7$BwR@erP_CF_W?s{(I#@-Hz?Avw2#tXnYn2U;D_bpCQzFssW0P$8eU7n!OYIV05?ec-G~kF*z0 z%sgD@NtZ!CdO6z<7zrwa+~d76Eb97f$7>>dn#t<%i z?V+NVtJUt(mX_K%xnLrpZq)d58`hqjm#G^IG>BWSm3!atYGJBwC zxA_T6b3WtFs=bXWy>P3kl#s+tTb!x=2r3H&m5X-ysR9-*DaPCNAr%aegLhsc=d$;e z2QIJI!SxI_?6(5e@LEu)Og;&plj>c#=-~vjZu5Ni7MaSaKYFuM4in|AFE1P;%=9@O ztmk`ox>t!$I|HP3qN`@c#-Gcazl_M6t!CZ*=xj8y4#l-;)_$@y6>h&S$`=G3b(GO7 zd~KVEEtGxnW?eYtDR633&#fURP2I5l`rySA@UD9mxMZG!raqmii?f)DG!XRi1j&Vh z{$b}`#WD>pW$x`~RG~a<*E?$XIF>vLBltw8gzG$hZbM6Lj|36}f+AE?&}ImUY(f2@QGnsW^|}JYr`Q2+%Ui@p%LgGHNT9tL#_~ZIA*H z2QPU(_2qt@pyqEvbQO2W1Oh#%%T(2{*W80mo4`wV-8IDS!N`mVbJ3b<%?~{?7>mY#?x)*&t6H>?EPB&P#o~I+L-j`!Rk=D|wwAk;t-4!-Yey z9}%_qI>ktQ&7zaL8~SxZik4qsV~~2Pl?_i2?*Er(Peuwx&Y)E^ug%IMq0|i-jVRCI zgmxaF#`gXljxO4d4p-NhK^Jy@n$v#1IhehL`&7^R?(`-8e$ChdWPHyd|5Bh?@38aR z1-2sUNZ{T5!>6BC-;r1wlu)}1uHAh`RM;?0>S#fwnz7?OkWzca|cvD5!M z{jc}V|AkGaoF6+%oy6;3OQLx}E1i3Xq9w@stqU;!Q;_LlLx(~ik&eLmt{w1}mE#}b zw<;Wm45$NDzZ5&@4%duYJv^K%G2!I_Xm5rIY(XZiX|6t@vTJY})GW(c4WfG@f)A_e z?e~EYvGG*qi9MgLKGD*p5G*o39@mnV7h2ilS%{zg`53f-y_X0@LagU~RPg7~LRrg7 zabe){XdkXXTcZpsmK!n7M|YbgVV3?==Ov3tLniHD#CC#;z^+UaA6@Lq2H?X0mWavC z&?IfhCxCIs=0O1xq5A#3MTFr~^_jmiLW^;6=N)^%?nJ+Z_3m%RIw_fz&|Np`KfC5; zS@ZX<^+GPeX%URf7~XKnOZbeV;l{6agB)HC7n27U9pCk6%Fm~5U5;kL}dse2jr0qgc_A_{F2kPy0W?Q zi&H%%A#_NjuxTY&u%p5x6#s5ywI!-;MU>Hp&Zjd7FN#JN4w5FG%}>3mt0CAY!yd9D z!Zq}S$T|+YO^Z|od;9Xe=gkB6uB^~0v!V)x@|+13dCiIWI(ObyQ9lhFOD!|YE02m= zcS%o9NN9)Hv>GqRP|Ki(yNo$BR<_W!HbgUrWg0Zs=_1B9J3VZ$t3IM5PXjumm&3*7 zkxPuT?_xXab63@4VXB_RI~n1Ey&d}HpOGSnB`8Eo=}fE_8!483V%Npof?+}W0c}6$ zqs>mv6l-teMTe+i-=phyUnDVx$>kr5bTOReiEsj5@Nl6-YLe=Y1+YU*A$%o==ti{8 z(4yI)=LOx}$`{=RXTER@r04cYy+A*SzKzE+9`#N-*% zu*UWWi3!jKU;obtiFZKbQT&>HzH5@IL3;XU>k2h7FR!)@5kr{d`-9;XJc~0mSeyX% zS*5>Yp9%v7WMmp@Yw2nm$Gmp6?lO%b*JiMFXt-33vEnN3E4^DBv8+MPQ4{Bfb$iza z2658N=BgKSZ#)`bvqGy8;dp-cckXbGLsz?`dC}(XIjh+ZAEFuI+RBID;s-0HfpvZr zGG3AG!MIG9QsXWs=Gv4{RSpw$f=hGsN|}cmlp7Pk5?}izmpg>XUzN!)102JKOE~+a z_tG^7xLE7<+p|lrc_kvESW2I-+ z1pG+sO?I5CtE+qpTcu?+{`V-?e_0;?(}{C5fk-JID#;}*czy~gDXS4jEWaFh$Jev- zsb^9$KHd+Oi65QDrYrcvx`FR* z`cxNj%HHXM@($_?#CuDpC@woC7%Q>qxEZv|(=(788~Tb%GcdF50QHjlPW<2eYykb_ zjnmB0Pp8;I)Rg@0kGNKLX`xBBZ?~ z8;b`kMTcaS&G3=p^|Ku)qLgNR&7N!vQ#q!!{uJR8V&eU_=PI&TD<`H06qqF}Og#Usy&StZ2Iu8xy?x@c!!0zmOEYaU-okFZ z(Q;d`7stJfdbxEDvID8QJQ##M^JIs5l_$9Wpy3_CJD;wxMH2$t`?XB&{U8}6X(>SC z^WJo1s2dOm%>8iz^HAW!WIuZ>?nb6(d3Dz-ZgP!E?S}{8&qkl#e=SsbqH6V)nr~)z zuLd&EiN4shnaW1rx*HBeZd4o1@ExgS$Na)>e=Z&B6$T%YN(l|cywSKOerZ*u5u|Ec z&SJ7IyK91dMg)}GEstL>?Jqfxbt+UemB^;*(VJAN^w)N72m!#|zzgS&2diCCD4u(m ze)8r~XU)RPj7StnIfgBfc-HNA8+`X(KT3`nkmlvj3yor{yv7lO63XcawzouxVIQON|%AUidsfLm1Srlh|W^@PMCTL&H2P9sU<@SI-Yk_{s)z#u@yCI4n#%6<_y}-xZN^D={ z?;Io~mZwR2VwVkGzgShhF|+1pvaWPUrcDdrpBq!3K{|1mJ>37z>(KWKB;SXJzT)6u zJBI(z?f8Q+hC}eLOZukaEC2KV{9U47ui=l7rM}ut2cyQE5w*j!LHSSCIyfKM-3vkc z?(A3Zakj%3sozvn{lX?VU@5-*{ld>DqIH~e=E+N*WSp&eY$Gqc_5?{wPRkaa41AYM zY^q+?@wJ}kA0efFWP>(32Ci}G%M18l9gJ)?ppZi7fnkyoj5NN!94H^=U}yyqq>hEZ z`n}}cTqUdZ9bPxaU^9)c)k$|^`e=r|8-86Sahi=a0d+^ z5P2VjYK8K>p=BdGz1o4B`093TTz*hzNM**UH*99N1L{fhVaPyuRAK**br=Pr3aNo4 z`mQX7jkJ`fPH$Sc9#Fni?bHlo4zKr_N(%lj_TD?Lsch{R$FYue6al4JKtMv15_-`w z^iC2&2`~x>Boqm~TLz>ENC`-25^53>q$CiUiqZuH0!Rr+C!r}-Y4gjRGvnwSXU==> zeedsc@8{n9k-f^^&sy18Ywu^R=Xt*0y(ogJH0pJ|7rsIn$6|2cdb;$Zp4RHjN5{J$ zARXydJ+O*)t#7#A-+!5^rkNHWRQ)NQ=!61d}ItLx;G( z6q1;X)AI&dJ&x&cci9jHOK29HX1ut;cFfn3F0mdgXmz{9cx|m!!YDp}(pF;E9%(&T zV$v2)NV&Jlec-=M6weRespbHuyNWHV!bgL@f1jvVY;jKQ{^iI% zQg+3t7v_zXppGIUsRE)wjToL@??jQMClMZo>6|~X^?MV$mmIsH6-PKu*ut@spSayL zmY1eA$#en-ZzY22CwcA3eAO%=atr>2F@1UwiU=ffe@ONKO1-1NLHLV_*1Rj>YinExIPhFaq9(ITIbU z8%84;ypP{S&C=^=whVr%s!)m&;xR$w2I21P$9y|`yTm+vs>XU!mt60yWyf1E4y1g3 zwppLnyJ5Iu=*3R8G@hDIGG#obwX+PTE*i=%$Ky6_JM1T(kN@Vc__g|&l~Xs$N}lG{ z=;@wXv(twUrS-`jgS0|q}<$KIL$a3iHR;*DbL5wyZ$AmQ& z4M3@F57nH9GgB^4{=)X;^qs$L@$a}g6zkUlDi|a9vx^lLnaCt*?NVq=;iqe#Q&<0>{rDpF zGbM0jk?f(3OgrfP;Y(X6^20yAn}_V6x$Jwi{7wUM~V ze;ZNEA+q@y`}4{lH&hx^x+|DrE`P4UR~*FdWsCB=iyDaL_;BzgwL!Vq zOkYIytf*N_Av67XH>zTV>$Iu?_#zD`)VA-2{a|=RR7DXT=j0FVJRLw#2vueGR(&eZ z=FebWB;?0pF;3I>G;>lfxoH8LSuE`797p%B*gCr^8kcWa(ZyAN6tX@As9jieAZkEn z7+2+P-I=+JMOwUIurE)~r1{q{=CZeHZ5J$kVgQR9hb}*Xxsg}YdDEF=TYiG5Po6%*cy8Os+_U&%7Qwm2j-oQi47BiLKfhpn*d*%$g0e$G*xj3 z2d$*nw3AOmg{Y+^W~2q2t-XwUVSGXxh+7*;&Zo3^B!AKzG&Ab!h(DjC5e%mMdU zjdRt^DKoM7sRjyXJgMktY~cVEYmygG4wao8tsYGZXYU}^iBA}!bW>BSB^nr_YEl$w zTRDSY1@gsAt;Re#WH_|*!>amP%ljgUSHiBl5dr5nCqJ5T{lwb0YL7383@3A^LhFST ziWOJC9)JJHooL1L1B&+aZoyM*WlMp%$l9~Bn!g1w^c0k6W9yA*;YqJV7>8tjxZB!+qLfh+L&73cL zDF)fh9l2DHT?Q3xu%tUUt-b!^TaNAX@mF+Vi{2$QpkLLx;MmC-` z(t@7+k>U>NS+cKSXZvi**2D}+U z=!X_|E*?1xPdSUO|QFWzR#~uWnvQ#fnf2nf#!nc3NZ>4{?-}CD!wDN{D(;>+e zW?JtDGR@nYXY~i6kUOu7kBbbxH_K-id8xvbwJv~K_RUOXuIz71E*#CB#Nb-WVm*`e z9UmZ4K}|}N&cRY%B_qYXy@TQSnPc@KrkR=W#hwZYxDXUP7q1Kfy#zmy7GBYGj;5qp zCD4H!&>2#})WxZlRn8CZQlT@g4(#8*xN^t!`EPyx^WgyhMB%)T|Iq&K75D7ag;5u8 zsD5PY@Wka<2+P<0!~3%+(O!<9ylpTQqx=ZMtfP4z#AsVlD$G`=9TDnu42eI!1>;h0C}c=9i7k^$H2s?TpcRFisjkkaptne)l102g%O z3+zwMd@@AwhdforC*w1#l9e!)5qOa)w^h`u)xdKZG|0QcfD?jM_js&J6GnB@%2%J3 zp6RLV!}5pxtoqZRf5%_t|2#;)HqLxSar+gZ>u>V@U~c*``P*g<+o7gxYxlMHyMte- zR{iUrEg;A$#53gU?qnY;an7@sPetv^4=T3a(5R$0_;#_!F3LRbm%~|_*(Oe(fXAf?uY`&kF75_sAm8~n3yl&*Ev6+70Jdz8acB^6D%WGLwtiG9AzcSf$!hYt!-g8(_XO(t)7vwW=(; zWOspVuU^QN9@-`STPu_9f>o0U-$0j%0$<*bs_X4#vjJ&hP}t#DSmLmuO+2mBGgVku zh`SJ%^o}~+=<)gh&kZi8EHGXy33?pUr3Xng7TqXosqHj$^Z-GSTztJYj1L7V4j4&O z>+`mrR`d~kFhel_g7Y$hT2CS|?KBi2MFGBYvDVpI$D@ziw2EPL%bO^u!{hFikdq;O z!`BA)GhKfoFTn5JW0(8p3{{ST#nZ4)DzP;b;&z3o{)YB67I>11gn|!84;D*L1zg=` z%%S6JjXC?Bb^R|Bdy|S|vF9}20IjJ4DM#Tqs7S7~v)2{9v+rB1fyD7M+z_kzauEPn zJJoKOXjfTabk>qUr-N(`Q2lx(qFn|SDP0I+7{S194QFVU?3lq z2WznDlmJYS*X|?mRmV)WlEEsiBL^hmBM4Vj!zv$c*rIr_xS$vdU1k}1nA)6~InMu) z!)USBC(OB*`&?arzAdnUJrP538Vj^AI;Dz_nnDRdt*ajEuSj^@AHvC!Z{gn_$O<3n zO~A({Bom*Ksa}O|p(JFG0tzpzo3C{By#<-GfrxPv)sM+5e0{lP3PTsjiyvmLk=TO?94j<|oYG#3L;$APR45fDJ`sq(ayI`cPj-H|;hOXJ3 zc6BfiW!R8hA|6BB=3pX8wmMSjJmb0M zk1nB1G&t>|kHe>MFW7xO57puqZ}^?%yIZ~6LBp|#0hzJzRsWuwy;j-=X9*%s#d5ft zrI4m%y7`Ri$?ZVsP5F!~HLMWJ!a6qKBG|!7Ll=PnR0X;=a+SPS!^m~f$lwk#$@BJ- zj!dlA3wR7i5fa+E1_Hq(r0Y0dbZo(DoD2-y@^7WaDz$5gUq2;m3|5Q{kW^Cj1&5|y z$@J&qJKMh4!~y3#;sQJ9-Y=ajL{nCL9YNEQtav04_=O|<4;dAk7iUA~635J~?|l2g z#jaXC%%f4ttb{`OEKH6bYx3r6m7I;yy>XQJs3Zwy}4w z4Ommf(N`nC%K??ygqa+Mw=_YrekER{Zz=rJ8(BzWjldy2Y?yGkSv54zr=?Pq;FHegz^ZRF7sscC28#KQnGM>5~_0twE@ z_wjvKXDiS$Rrqv126imi?g8F;Iv0;gcIs2i4rmhK${=af7f|k4DrHGh0)>+$1trR5 zd5f)3!QRHr6(3q>cv@2%!3vAn!4z9@d6J1MOjl`F=j0_z%Wyqv`h+sRvV&_-L7TaA ze;_D&pa$r!q2ne~myJp{!3_dc#bne6zkBlSJBU=Wq~pYw>DHmu*R6Pa-GX@p;*K1( zK18bbvI|$R%Rsp_mgJSMb-Vz!_a9kouF`WA87Z5uL0KU5NafgD?Nrk%~yL@6a z9Ln{K3X7e6@LnB3DB$Dao5lNDKiDkEAGdG4Nam(Q^*}do_K5R&@0P9zOnXHFjrtt3 zrSVHf5;iJt$6YUc*}wlV*7Da&FIc(IpSlWbGaXPlmc&ue;&p&(uuAS@$y{GNV6>l` zJDhw5ziBz0gH|KQ*~N!WYwo#ufL(i-b1uiF;vmV^MB`}OMCO2b+Gv5$Z#ef~_UP;M zo`}LdXysqFt$I6Hb5e1G=S?ZV&Y%8$+;?1CP*PNz z>%CF8dBNVfrNt&FmI10)e55@FK%^N zst)S4z(?jJG6GG@LdzxmB|>=TQY8F}kB%u^oatKUlX0eT$Ko4Mb5%@y#@x@=r~xJV zR)_VlFy{x@$`mRp^<6>#bq0@IvR-jK*I7aU6KAU~qNLMOD>L|h+D65*!O#BKt3UVr z_xuay5RbP|VwJ^kOgmILmAg8%)UzbmQ2^E_bLaT}Ah#$jsRgrIeBm_pN+GTzD(w1C zloNtJ_Xamg%#PR}&r;>hsn*19xuHTq^$R{PCJ%JG#Y^Ge0*29#X7VnLFX^1zV$5-7 z7Hple^176wc!q{y7|-OCG~9G_qS-FE7q8zT-ub;N|MvLjZU3)L4S&O`^H0eLWz8NL z=FUl5L~i_9$fV1oCEtK(Z6a~zN| zFF0cB6_sN(#v{_Vrwr-k3*Q&sFq7N*ad<)s7JM+yb2cKPj$(kBxkVYlb!RfJ=~*3? zFhQS`18FSggYRKD`fn)lPo!MagSrKfJav>N)Dk!1RbBRg@-P^Lk5Ynp?t?=y=IK$x zUUs}EhTmK_Zf_a4FY55Mp`k(&&Z~p5-L&O(SD13;%yk!Na6STQ!gh{xB%d>Ky zuJK>;K1zs5|7fY9#UHA3ayYnby(-7;^qHyENgxj)%%%dllEoa|yi;@L(*oyURX&Y2 zgu7MfChw5INoB;k0P?jH7L<{1o}sLd#?n_>hpcsH`nBF|uGr_}@54@F$( za#b2}>rT50K@r*?=ZDT}W`7LnqhtyAIUtK^s8hG)mQp3&frhPx?aEFKnyorq+}d%jo7oY~JjG z1q-vocQ)2`^}DHKDp&Y(uawH7rhK_0(TnfdUmVjv5$=~NW~8r}0Oai#4&zD>mwr*9 zmm>uYjEYv->H#$v*$5Ti-Y&CK+MR81Sg}?$U-ZcXt$n|sXnDuXdg{6^wWqpSPdSs2 zQY>glg3e65R0JYNhF7h#&$R|nNno-ERfl^*y~l?eH>Lr$t?*Vb9|y7gpX&0!S&!^4 zYOHVcuci^x>^Fs4CxbZ82&1J|b;%a}s>6!V!n%YDrvkVT;;bhrblS?3*r)6g<7r<{ zEsXJ5#;Tggk4%9KdD$NvU((77Cl}oWvIY_B);ls*XOo>A>>cR*Ee&C+xb}~!=cN7-23#i zwI4LXJ8g#YlJC81XhYy8Qn{W_=R9LNS@~jJ&DxMcD1M?KNCpVYhR$}<)?@<3iuGdK zR#%(re9#v*faWZ(j=Pn2{?x{Ql4b4le|E>7+0Az^jk_6i`)k*bFP!ZUfBFK*=)1aq z*AVK~u9MEH=CxwBJlZ z2>0YRv0*j->a?Y$KN66gdpGJ+exO}UWXB9(jC}}9o$q@)-)G5V&!Q}3Ox(rSUhcy> zuJUG=5QGy04JZPSW5c5ZO7&OW{Cl^_H~QawyNAcDbQv(`RJWyN8xj>c$7e#7UVrL1 zXPNHzVZ8QoSIN;y^EmW_ZWAV~Z{;7tXgI z%yp6t#^?f$u-@b%LY{p(`J+gff*F~e?9MyG3-!$W*`Ttg{6JQZ8PDkw24aeslkBUw zvev2}wL11Q3sd9g`rC$=Bhy#XBEIbCw}=Y9tp7&(7!Pt)^!9+mqTHw1ahKW34*7Ha zlCv7}i6?MQ)}Gxd0mm^Ik!*d>sn?MA8&`YfgsuA10A!$T&8Dy3k(x8{t&yJq-Qk)6 z>G+LRhmLsbl!*WYOG%c8y08{~(cBa=w(R2X3Fr1BGe7N=82UU`bS zscsQ<0_^T%5`E?V&kIQMAhe^f)BB0Py+L>_(yypi)HUvg9P2*Z9O|nNa%*~J$ z#8n<28(3kRB5{)M1-rlXjGyC!ANfONIUeY~rTZF4!qITDA)*UlYNNwQmw6y91j_={ z_P$6XGtyLJtLi92*Z5Qo>J82EH65ju`*#8;J|0z3*IdQybl_;9$)A+b>GCan{p4E*Oh0WVnvKhbQ3T!6$TW$ zjD(DTkdqW0LM^p6EsF09A7K;Ng;AzP(Q(68Tb0YT6%yM;A>*U;=TE;=p9*xd{$#$> z@T@xSf9nfQoPZohC(jw#QM)vrLHkAA&BzMXN$@h!VJ-dw7TAnEMZ=-w&CtaO4nE}z zhz%1WsI&NCLRE|?1yec1yII;GU?pIWP&z>3?H9m2+kU15;Lo!C-})E-&;4uEy-j0m2^Bv*yjVRDePJu1)G4?|hlLrI3l}l1 z8h~mqDVrP_x7BH8ZStE+(FUwq&6j9re_`_k^S<3pgQ|%gdGw|1|1ka8fH(2vvuDxQ zmi2x2?bRXz4cDM*oKK1qjy>AExS-q%`Dl4Z^xfBO{Sa(G04~@JFjqs;>#l3?R45+k z+=pnqnNeCTVLrU5THzbD!B+Es&J_Ra0e|iYnR287OBpgj-y_L-SMze7jM<8>~nW(T`xF*WqF|p@7s%F4ZCnxr5?f(Mf{pqjhP(Xj#Arl-R&-l ztUVxaB}N#A)MF*f)0tc{wmpNoUHP{v4Ul246ULM}jW=rH_^Hl)ozndH_wO^mh`(5` zo3k>^sIzog8-8X@7u3d46*kgpNGuMinFm}K&Ic>z7GZQ_b&-O5MeO3YmH_~2r_DoZ zE50xl?%;QJ!j{HO8OTQvlQuX~&wCc?I&sQ5$oH}W4LbKNBu3(`<>Gionfk7<#squ-^v5*& zS00@=e^h=ub#TgrgM&jly5qs@p8d=a_xQp4ut9mW_33!5L+GPV+=NZ=liD8)FgddU zPrSnf=;5tsBh3v@W!{m{X?)7lZwvjsbYG7vCZ~1%Ze}vqIBA-ocaAGc3M{SMO z&(>31u?XFKP?6@Aj#y5fv&?Zc^dT*E39gofH!+@^Hh9Fa@7_ERS=vx-1<&v{CUiti zE?CySmiLyTxBM-o@jqJMcOS3(X8nGz`R-xUI&Gz#+7nKy(71Pb|9mRxk{2JI9% zT+Y8CMQ?t3=6dxlu(7(rNjVtk+KR#ZF$E_vNec+P1c%=e;P$(aHA?xDHxwr~-@^f^ zW69a&vx8m;DlgHqw7NC&p;YL#Qff@Vjdra7=LD@@zS+QQ%%af!m(Pll`e$dU&aMF= zYrffcx5{8WU=~1?kjnaex`{{on{>rZgEDPqwNKg8(j`~VPl5qWm)t>GuT~o^=lwXi zx*@6oA-%^^42Bkrv0}32ufr7l=Yc1oK>#XrwI%k&xUOJt-^bp75zG{o2Nru$K5Y|| z`G)iAKknHN$C87MtNI5}zMQlRMYfjGfg@b9XS<=U@76{cSJ4UCF&zg|k%MTj;3e zR-4S&upUM=7G~uccU5INTN{;CMP_DgH`Hs9LopBSLapGAlr{*{I?482!^_NO>{^EC z$ov#Fb_XRZ_tmO~- z;Hw<(zY_gFu%7$%dZW*bqjG|?*X`D|UN*}O&0Y^$SAW^~xn%!7HnuN(45}V2_?AbK zkEc|dJFkZAltJVZT}b&*$AkPc4|s2X7xPCLgbdUt&>`3w2z8~z=I5t2GyU_!ii32c zqN4f%4um9)v0*`_BTi+;mSRe7IXa_Omz+5S@SGW@a(KMFKZ}ShJ*}>wm%OfOaFcBM zcA9u^M#c_!tuAY03RUJdo`GMbCxw^GdS0m;4me=#p(5N$JM0j-sErt zF6&#a$^zKAvlr}L5d2crQIS8-LS|)MOO5-?;N?i1bAARtH7D0R2j(VZ(ctkqOnSQ4 z8(%`yfSR9+i0mL#Ju#&07fWzy6jDXK#ZTL@SEg$0Ao4(uT>!E9_Z^Y@tz~J;YxAUG z4A8D5{6l@x7k|pX%$Bds@?ZUXpELJ{@^sJPTR*ZtfHzKWcw%m~%=ve(UUMtbRl}#b z3>9X0@K`6Wker8K7n2vbo_xOUn+d6n=ybV2pm%G|PWz=386RgL2*8+%%AtPnPN`KG z^cOaB*o`$Pxae3S3z@K0x2}qynkOe-Gt$>zmeQp;spAGcdjH;-N#qbUbP8n4;1POV z6mr#t*#?>>ICv`=0RSGOi4_oAy})x%dHlZFgD(sJ-jgJm+3rB#*a#FC>O?YenFG!f zM28<2Ye^aiRi2iQo)0&ti?e|T$X%9}Ji>NqXh%VT(Kb}uIo}io`egrF%98`BuI(Mmu#wUIsW>>Bm??|w%r@}e0WnfENW!XS|H61${i z*bQPURtVMuF@m4C4K!@Q2-gL=S6`S<#Rkq$wo3Pq@3-s0_6PF>q_4YrwGy}ucItz;i!xCB?sbHA2pl0(^RlX`h zdsg=W^E1KM3on;9V6F0+l6AGwr?eWlYt5r7H6Vrx9qelC1MbCnRaWmv*wjR@;j07@ z?i$|S>0j8k_RK44IV#CpBly%+Z>7>QHBF>(U25m*GWi%MA3}ZRT_HBd5sq08?l8x% z(I#dCZJ9+``+wm6h3#g0@y4g62HOZ_PS?<9ogskjA9vZN_}sP1t@rk+>COQd$rVYH zeaQ^tc{jJ2z(C(m&JP`PnG;!?`5_HEDIbKdemRMLPydV7`1N+`S8hIEy_bI%@HfXj zkG`%H)JOGsxq#v49TOMkZl z?(6PdLUEilsE%1Qn4Nb_Q&1JKT#k60`F{7#cUKQwWeb%)_%D9Q|4nKD? zW4ro!gkHDpG9%Qc;f2EQZQ1tsD^LwBbCQGb+ue%o3z-p9FQKe(wkuwrI$1naGo6cU zN^EILK@eb^1x$Q%wzO7nm$Pf}o zxM0JZr$UUUE;?|fpZM+daiGGMv`S>rqbhB!?N^0}R$nsJjhH6Mgk6=pipWTmH40iE z^|?Oe*t_ZT3!B-dp22tmCA-Lik5^g%%^ej%U^sw>_NuVL`uBG)T(i#(cI_Z>>zV-s z^3X%9cY3{$QE%0hmhY3Ox=}Enff&_%DsrgZhr{G{9R{#xfvukbbj4RgdYuP#atn)d z-FCEuxr;pfcz0bsfSS~T1)u+6gYTbR3(l{GnK2>+=Hhi0mWMP{+_Ll1*G=~~KX>jR!U zT*H?+b4zF~RzNJfLNko}3Mlz6v{*i4c@9uM2=I!kbjywSb4>xd>8DQ^1Ltf=<@Dl> zBbV6Kl+m>zaRxFRu}#ZqSt=uLA}VMRpa>(l?Lr=utv@)+(`8K zzG|%0`(=1n9cd|n~f#Dy-MK^99Js_ttG)GKIoF7#rpaQQKu@?KJrNp)3Ybx ztPp0AnuWElqtxeb+Ap1=nA-=Tq)`1Otx>%!jYO)>+^3W1;`DYqQSWE*U)Ww2yNmw9 zre^XB+sdB)CsEG3g#_TOdDxJvMx6#+?2V-O`Fz}q76!jU!CSIV7Og91zCXZ{+_>D> zG_F|?7X3h&bVp0hV(rH{TgT9}8?JTM7fa0N7JCnoqGAtrOcy9EVoB`vHLvtO>w6ro zSW`VK77_10?S-{$baTv%b>GqrRh6HGHfLx9<~g^!q9W)MA*d)y`lNO0Z5I$rkoQ8YTH)>`CzSh(x#r8o!!)jU757Vtb|)vfENUM{6- zafecDXHbrYS2^%V?O?tF`K|s+!H>5gkk&I>`F#wRP3BBKz!lsgcuCCCv8UEPfdtD% zyiGf|w11QT11#A6=X!*fZV1j*FJUv`owfeTBta7Wv5?BP*m1dH)zk{He>612yg_{wk%9=?#kjGGi01FELCP zT{kM=Q{vZWKIWLsI6irvr7ynp9z>5NB@EAFgeE-8G^ao$3;#=K+}iw2V29al=t!ef zhl|gV_7rD~BH1X%g$6x7T$r16d^ixw<^6W1J_nw;Ut{dG3xsx>mfY<6K{HbCwD{fl zRX2V&Y#=BF*U-6)_CjD$&K^+2M}IlT-8;dQvh3B`tXr^vIq#^bWRipy$_Wed^prVy zyDkAAFm0vbvZdYcWOdBiEb|G*fA(U6#radBnNv3b_~QHkpWhen4qo80n~v}!Fcu2iqa$nLTc&bN{K+IVsMuDFqyKn@ddIETVSsz=gwdVyT(WO#vr zOh^gMyjaq0^-Ak5G*O5n5QY{0Q9?-p;HYv`5 zR6VA~ss0^Ds-{K$@^e_asI7Zy)K}p4!_)IqU~m&wApF=eK`q0BN3#1!(~n zELiA_8Z#P(2UtKa1Nq_jym)}@Le-g#K*t8|ABmuejOmGUsmeo8Zn#f$_N z;kF|cHtmeZY3(*)`cqt0juJdaDuyqQ?!yp_+maSc^Rr7!xU!+uxGYz;#`!&EFQ>2= zOulaPC5{+})3MX2sjUvsEYw|yFn2D)i9lShb?kDpo`aHcw<|T%ed!N#;2Ez)*;#7s z(w?n>hV`kR`k>YFRf3m@*hu?aT*2L^?iI&;IEe4?L?6%2BshL0E8~asx{-bE*^w9+Oz?0jfV5N5_+4QY4dP80+kJp-@)Abj4U zqbWbn2cK|wPzMLNVPC!?WtSkIZ@G@YVFIS!xw6#9sl9rSi7rA6vIeXWLITR+Yf?Eg zGP2-7f9EV)ZPLr7%SCtPQ6}QaLe{*UMC2uOWOtt%o<+^=TPwp=5pdW_==DP1mV>NU zl8sJ@;u)T*&J9%c8AIlW)gHm{giLN-1rgj}-?gKzv2q2{EEl54IaUm1z+gYgrOnoD zG*+A5LIbzFc>gTx2wh_qGV>k)5Xy2M0@$R2_1t@)Xy3>9^QXOJd)#w!5TUg}Q8Z0? z(OAWCT!+7$S6{blfsLtY!ItQ7!ZY3$p>>bpnZVBQ=Y=Ea;VUkqXD4Ds73z&$rkzwen-I_#R!!)zV&HR%!;6*d=G=&f#zV6jm&n9RQ{8=F|*)_jJZXL@%Zbz7Fx`AKWc4{*s; zf~$XGZcmMsPj(^cv_bW=tMIcoQSN#A-r_;&S#*u8{I07BuyU80LrW1kdJpZ`}EuMFNp>#wr!#aXx>NX{VI zOTnb#fi=y2ElT{t#5*!WF3;MKUCvI}5%0z|fXPc9mF1G4UG{>@g`Eo+QYv{}WU#jC z4WrEEnX%TXTBvUTBQnfJ1qz+5G>o-if6~M>Fr6a}4Z+)hjUQZlqMIq)XUy~snj|4j zN7dD`gdWT|Z<5NO<~X7IS_n~Bhtg~SmY5me?qUF1D4oA;cntmv8<1`GFvbrllAl)? zQ~E-NQ&9lrz}u@2ki2W6quGiV>IryeGQWv+)Y*SD8hY(nN4%m5R5`u#=0p|A^g(WM z7a~T)wWgtZxA(B(g1d~p;R*MmNyu7cJBp|d*{O)1=$C6kZPm!%Zjs7dN_VH+G1oZ= zPWikC9zFkERq}mD;BqgdVXq20krF>kv(46^A4-jgX?=6Lo5Z72Zi=PEl=im7@~}2C zK0^JrwDWUr5ArdK#S6>}Cv4mdsymv(V7n1v*fI&Rnr(%h^;h0n;C84)iv39?3-!S* zGaxkdePEbPmiZu3@`oX(sbEkkA<;i(f#+xFIN{-nm;FNs6k?`ZO6wHs4h3itQii~S zc?9Q$s)Se)-q7$+Z|jB|enK2#_+PjSh32d={bhg{j~-tT&bN%iaX z*0mjB@y^8C(W`ogJxeP)e-w#Y-pQ}~C|Rj9o``dFdCO2DV+G6J{nQuqj8R-Cx_HVj zr)?LrHC{I+90deyrmI6jtmKTQ4Ulc?X+X50uRXprHd)EUUd+}9vCo8fQ%G96mU znnFn7HpS(e)tb5gtOal0Sg=g!2}pfE#Pqi~L!S|vNc(x30g~AVBodtHj<=5sk1fl| zDK5f+IJE5^l0;okXb>0c&gn#;Sr3Dqp4_yB>Q6h*ZQpy&NS?d$@!}NZT&{qijfy%asAp)JQGDuKSp5vOwJSruZ|Q^ zl~yUrE0%+~P^3dWKY_XdWq)vb8uH$Aa%89h7d9iLgl_lgA+{Jk1L<7Qkq@ksHWR-JWV4%uxWjC8&e@T)Ld8PXv2y~9P@0i-K>nxVA37Ut9EgZP}JcOjfsbj zX+76LA?z-N(c8Hn_im8k?v}M8&(=@&jOuktbm>{y2@qk`+Yt!a4YY+sLE2{Z;|>TWUz)I1e55qG$N;)GeWDCkL61Xxbf6shQQy$Dv=o}$L|xT{%) zsN22Hs*e?T1P+a67e&K&Rt**v7vl?U6wkURAhlya!W&APRw{IvYp7-pkn#&c-(Wm;}Cs#W;n^olq3?Q{8SegXB0;X|Bx#V z?lX5!(V9XK_>(VZyrx2+sG0gHqZYo)u}~sMRFnV6FKh)}o2)29++t}rW6c#P1~-H8 zJAkv{_~?hL{6a<<);1;GEgTw9h+#xr$;~;X^?XG)X!U4U z(}^AyqJHOoS)?GSC?@h}n5xN1=}JS~i8SMdq?qpB~S*1mR-Icc5bzHcmStS9ae1GFM}dg(kN08H$ka~R{e zEP7TZUYe+%6R%te7g;Z^G9Kce5(VVkjea3dXGO6p_V)=rqLg@t)z#4pmI)hyZr;6t5Wv|CMs^jsC141&-SM3=k-l6L7Nstg7ws-po@WLm1P_vU*-GsfL93N`ks~+9n6S8z>sei zWWd>(AD=vSN{ip&aT~fN-GMtYGlaKMH>xgqLVFOSGbAR#Vo6)|JI1c-jP&l*rUX<{ zbqcg{>`^2;5DM$Ip@1!ZpkkESehCwBq?X5i*c~wq5ay&dPqGYoXw~d1c9WB7X5xHA zzBK5qlBaKIRhfa&r_4ya5ACwTSW8?EnRlumhMnhJOKDw(0~VE2S|tPZ*bo-eVq1Vg z-la^@jtp0bBt!%K0lTzdhG(5e-Eds|+llZE;arEg$cHqx9 zCr5gET(vSsmSQ`KVLW9d>rm5ApCt4={n^8P%i0+i7y37JhYT>8h_m#gv4xt1v{;d< zsiyNfN6)pQoU^3qf zy_WfrkxPDshFOSrEjQz2{o7K!>BT3kgY9}aI!2vJO!UCEwT2}n4+|63K?|tpIV=D} zs?qhCCEPTjTI(iiwE*xWiifn>Ep`5RM5%KEuJr`SKw;XE znM%!9sl>pOC!tV61J|KcknQ*AeEdC92Kw}JskTija*(QV{aUo{P>SPX5k`EIoNJks z3iXXQ!4r*i*uRh4^R5imdAw~A9}69hzu|bdn69ytXVS9|RcsYfxFALOFje)kAE`5$ zu0rwXsHS$PY{)ZIy;2Cl-Yzt+HSq*2@A5;CP5+Uw9U*k~DxLJqP{+i=fDcqgZHM}x z8d><-%B+R^#H(c&rkbAd@R-(;8;CfvIQFsm*XLaFt?G#0%(hru&4_zkx%0x?ZedYd zB`koXAX-WVxdnTV(O-NZF3;t^vt%H1@OSai0^kZzr#Ht*6PaP~hq8V4FwrDwte~z` zVMNVr1U`00IZw#Kt{`l=b-h%MpO8LP&+iKbbrk51=aqp;eUGzP+oJbAfUV0Wi#6M{ zl!sq=`jIWG-@+w5{Cl1kEN0l@26_buZm6!Dc#hu7cv8pm=C+aMn{zF)UY#-Rp3o1h zZL=hsfun*i-^8YDou>%ANLYaeY_N2))WH!){ z`Krm4+RIc3uvVEkPPHcdRFrvU!mHhZ-51Kc3Ps%rQu7^>vXV!N7P=*Im^1+aN{Wh# z+_4(a4pj6G=ayE}t)z0#x(QXNim6goLXdml$u{owxuiu;Vfot`THXSQiGnz(!pPB& zxNol!V5alG>FimrG5skE@*6LHU9GfQE}iDWIMS?Urfwp8qpD-wcq5`80{B+|$8>w! zV^{Elf(dLN;BKe;szgLZ1t;Id@pRB(c7TZVlQJEJ*L5C84A)kk0cJ^VBA24Je$KqS z{0kdv*;4$EWs4p2%8tn=-OX%)19`3yi6Y?ly~sWsx- z?m3P`6U;ge;}}_?Vtpr`C69JY{K94|{ruIZTTg?xDZ9SsH{NYG1cCn=voBO+fg^#y%4wt zu2d7h3>CO{ol)<&awU~2t{+-Z{+h9v(`A{tq&?U=^y;aeuv_zF{36O%ChTSKrRkl6 z(?j09+^n!wM6m_oNdT5g)Y3~X?w9hYeMx<1!zXA>c%cw`xUxm2HO)=@uhP)dX9&T!~O6RqDLb#M;Q4FLfC)>lj}|{X(a*{ z;l*-W!#GyBSQA4ojTBy5ZPa%3v`LeSG4&?8<{Sz1j$})kpr3rehqP#ssSb*F)3mbb zZ90kH9)d%Epj{mpLnnYHN3=?Ynpxb{hMp%h2=l&&W|A{&qxqBs@;u*83z)#ZZ)naC z$|`MT*y^FOJ<)bCJYvqW7omeb2|vaoWsH~8jm zGI?$;YbY<+VWF$*d^IBMK-p6`fk(^WHZmVIM)c%upIYQ+VNQKK;W}h&C1%pN6{B@w z0BRE5LLGq2tQ6)FYzAsmoqa@1eY#c_sh|Z zI-J~j4vH^~ii#eF*oe$wZe_h$Z`~2z*ZV<9x9YLUc4_l`iPWHg-+Pdo!kOSCsA39! zP@gpU2o;sdNoDPcqT*f7OebSrFXD=Y#cWFaV$4hW`*g_87lQvk_TD?Lscc;z$2ty* z4q`+pI)Z=%0qN35>D>^L&=CQFgc^DiMQOnVDWMl>A%z4fp%)ba2~A4qp!6oa2?+i& z=bkg;oSAd)xu5U--M?=B$zE%(viI8WUi)3|df(?UUk)z>g5yj8X3sRn#o9kW(`R8^ zQ#=OO_kV`&p3A6Tq+xV8D`&j}cg{L(q%w9n2o>L$7%;MD5t-5KDQ%{_rGD1U(|SDg zl(wDBb4qQCi$JfNpED?a$$c1$!Rs)7pTXwHJ5_Lm8b-a@jcjWuj}qgcc*|G7f!V%B zsZNE5n~WW4h8=m>dZmt^fxF;HBEc6(3*8XU()Z{9iAEXNjmV+Z@r>t!m)D?!IDtos zan0@iYZDKG4u;zcA%mjen->7>u||^>GnJ^iSXnluqOBUZkVWXVb?y8Y)8&=QS(SsM zO@Rv{vL7&g=Hw1~yw7RAoAEDlW9s8PKBOs@*o z5dGRa^m<|5)Zown@6lkExAm|Aay6P*Ar>6~YDbvPiU1fk%7}`sw$Q3s4ewlM2tegw z=!i^8O)KLm-KaYuN#ka9F6>(ClypfL2kFM~sbnyM1IvX~=ib_}os_YIh}ckGRYGi3 zZ6lIDMGf=p2P)YmP{TIwbVDReXzz+Dieh(C^jL=ja5|^wLf2$qWI$is__+D z?Z+HpK=}fwKT&W{Qag@+!=JtM6C+63*Hf=J6TASgTt-jFdD(jaAgrA*0p+KtRi#w* zSN8@H4%1mtG0xhG7965*6kY#D`sGd;nPWL;xqR0feH5)`!=;^ek>k`YZsmonJKz&n z(sD9z4^>VJ=D-w}RaKY3dAcxLJqel%-{vFN{fRkAob$i#y+uu-KShkjmUhMDbS+SW#f2PEqsrhhY~c`{=11v-J_KadG~p zf*cY3$*^%!J|$~#ARtzLGL_(vp_01bky+MxQKxHA3Rl(y{FCSY|;a&A9rqLRbNTCoRj$QbLmHQXa*Z zOlC8--d?-f5B2q&K1>Ke$c@Qz#1?T>j>b?XF%y10zI~h9jPP3VF$%c*UB6H=mv(Z@ zfd1{b#7U(x-IZqCyi_tnm;_r|NR1ORRt-H@K1j9|OGP5zR=T0)EF+glzW%fI_cU8J z(>sjEFgEx+4|6&VD8$Z;(k^5d7FNpCK5bW*1sqpjhE(xWEgHosM&=Z-!hGjbhaQg> zX_|z_4*+Id5sWs^ zahhp$#g$0UvJER*+y!cWb&o#X7&$W8IXd1IGE&2Ok-D_qKd#Tt(_=bWShiC?U1H%{ zHY~)t!%_ANW`odZUKa-j)^$ynAx;B^2&KJVOMx951s+_DK6y2fnZ{U4isu%`VK#2L z#Xct=?}=a&!y?2ZB2f4wh`v%vCR)#@XoN36qxBBonKj^wHQL5JojHO&t2VzHW67nf zs=p&PJ19Ln>>);0{j%dK4|BQ+^0T5rjz2)Dk`3OiueK zEVBB?9)?SYltEKHDX2l+m^jreC|;~#lPYutwwb}0oCf5gbS&biVHC_s&C0jHWnM1Q zJP%c;JLa|=zkJ6xrbcnwq^YfU+vs3&w4;w4$`mLRbRo7FM|3*)k%q#ji8WMF3>=!A zipz+^vI#6eLKlrjMP5gJcDJ10Pvr=QFFml{-0rgv*(`}Ji$)3*_?0o{piT>AqWz+w z+=Cjw!g|NA>E87~(YoQJuPVM(3&VUVULnO)a^_{@0lG(A&s(>x&QZoXrBEDcBvtV1 zbT|c-@6+J1@4LPZoNu=S^Q{kIW0W+A>jgALz@SFC0)wPsf$&Bzwl_t51Mf(>k(==m z+An33*YE(57WD?i3J|Y-XQ|jAv^H`k4-YR6c@gS=~rh*r7by+Vo z+OZSF+IOlGr1?ujdiM9pwjE&;jZ?<+S1>jy)no>g36KG;pK)1IFSknjz)HDv++JWY?6FanwSVS>X%7?FIPTCWLDKSzO`n5 zjW7d7F4|WrMmJ;vrP#S>=-C7QOn3&1SYQVC9S7}PqfrmL2eR#xvniNQ_}Y(-0c99r zxg{&P?H#OhTg#<+X9R;^^lk&97g{|e|9-DedrFt&Mn($wc)3-qCL>+*FXt?}&U=P_JkN6xXeNIE!#5f<)OpWQ&QhS zklk9}$(3JZEKPhHCNGyeG4+3~oj<$xIe6^WmxkYQU{1bf3I08$@LLgvkJG=Zz=+xV zJ2v$zzkqJ;tpP?roAp{mICy%y({{aaDz7i>DR$AtfI_@{@le)$(hq!_X3yPwEay$( z?(gNp6Kk)1w20nsx_XJ_F~dLd_vCktJ{~yozf|YP-(BkO)PHxGk(RAne5j9wOL45`u6A_cxQm`dxYd_Wv;370~q_bxy2JXt@zqUqCbbGR1Xbj5NrJjh#la~D+sN6SGG2RPyY`5XSLcpBLxP5Peo(glz z-#uzt&fsTd_Vi{AELg5OR+Z?Rm$?VzX7yeq5|e+{dw-vdUh(Fln@h0^y#WN-V69U$ z`J7(8|1$Mpy3hs-ATf00`*QXMK}H-Uy^Q$ehw-ntdR{SDs}p)C5HC6yVpP|?8Q7y} zx${61Bu;8{i-=t0zRC2@Fe6NJM@36X>CMo^eeVu37L%6~LlEvT9t{^_u-VX8dD(8y zK#=xHrL2;TIYbPMPLwK_iYg0#NtDt42>m7N`YjE5|*k{;{Tyf1yf`-pb&R?a~ujQJ^Yn z_sw>uZw6dS?o4-9NnwO~WnWZeQv;N%7!!Vm^IleA-mn3Z)sTB|B5Co-_y_-mXp6zC zW_tR2rV-bth>p2PSsQQhB+JWcbLIl#tDoR~>BV{TSjn;jDHE62LkJ! zHNQrh`$#lnFr~b%-+rkyG@c$nKa!1Kh?6X8Xl<^=>Vqz71o+t~|8iw79S@JvJMEH4 zPVehBt+-iIOf6R8FVrsa0?77cdS;urY{K>xrTRvJI$DP9Up_wf1pwq2!Fm9Bd=SNR zl|DogB$`GY|DYtx*a0Qd)eer6?)UR|f2P#o%2YA2s4^d+(YAqZX~ougJM$lA9bNUT zOPyu1c$emF;tDA@q8hwd-RMRWQ5BdS^~|6by*l^n;EG3}Td-juH%OzV686iqf#dC> zu{4yUDVs^38j*VP6~4Q?HMd-28QiL&TP%Ge#0r-Axht%{j1>~Aq^--Yx9L5OY>zNJACi<}I zO26NnbTZ{gAwoQs*^RUpDl=!(3U#RfSoBGv$0jK;w|_5d7U{Gc$v}}2rb!}k0D!D4 zKv|n-uTjpsE~9~0kkw~0&nS}By^*say7XkKRp~Nx-^9M>B6Is4uxb(^HbcGnjLT%$ z3dMICAX~+L`E3T7yApU|-Z89!)b>%>6aeffffH)F`D_*o0b%@3 z%YMnBv1ZSrc&m`;2Rs2*1v%uf?(%mv$si1v4+sh)V%Cb8Piwu;TiJzHmcE=Gfu z^&E$}_4d}7T@APluY=wusZDIDO*TrjrMaiADtV9Q_o@*DZ<-eB^$q|@Fz*DIZk(D> z$upOA9v%@0TbN(Z?UrW)PYnYorG?2A1u3(U9*w4{o#ysE$bHM0g9Vh0S++1%SZ{Em zO02-&4VvA^Lt*bh1ih3-dz2b`Do!+Lfz<~cmywmRy;VW)a5m=eU_=+nHtRwK&Epa$ zrB5_rh~L8I1G~&1_b{J%kBhv=F8mz=8;Oxcj(c=GkZ#uk44XKuqsYl93i&~DZC{lp zsAUO=)w4JTlyqLc489o~;!X6Gl{vEAvs0z0@gS%AgYu&kisH6?I;o9p#S8?BhF+k> zj3$n_^o@qSfjE?rJF0+@=YjfQAHR#13jViHMElJdQa_AyOPKGY>Mr*ko@y*{G2TZY zjF^8mjmYEw$$!bTgEOpmg#%O;F`k0L3V8WzqvpYL7Q}Kdfby6?zi9e)(avaoy8$UP zZ?i8mW8WjRntr3Dn_s29!H?mL1_HoNq43F&Pxm*Q>9V9CT?4wV^pz;7S+hR9D@#V@ zq6}j*MBb78$`GDorgzI@CB96@j@ARQukJC=Nzm3BiPliZ0hw)`<>5}(=P{oK^d13*?!1<>wuRE%71Djugvr^Y9b9_=e zlHs^}EMxf(_kct`o6YtjGL#5xJgQz{b=r+E9~TtcE#6N>o4CB9D_O`N1IX^A#JZMy zvHC-lP`#G%x@*$R=@B0w;FdF0g@&|dD#%QjVTju7GHX1e{-U7b^G64xYQ+LCVY{*_ z5z_?5=6C}TnDeS|bYsMLhO!My_P{U&lr`!|r<02p_s!S~o#`wgd8}@+Z$Gd4JOz>u zKR#im@)=3>xeGagA7M%u9tfpvztoD%KW``#b)YEB?UL-`(L`!7UfRK^Xq zMaMndbMO2_a1E_1mSR=DdnA0=HH&3~q0ODUG?19Z5 zmMbbVd*&6Xxm|4E6jg;H#5Z7XExUNqxGX$t%~!CB?w%D!eR`n{vsg=V=Uw|`g;U0R z=`K=5BNXp}IV-+1?M~fL9+A%mt|zImORX`mFpoFC%;gw&_XivdAxcD{YULVw&^f~O zkKdSrIXpzzqr(+i(>w*oU%|%D{ebId}G)F~Sfi#NVD z`Dxqp{-a=+aF-N!SxUm~g|s076@H8B7R-zQEw^Uw&(n}wNym1xbLd?|d3x4TT-2KC zO1?bZpmPcm&js=tZa4W(4e6PwnQloD>DT2lHUJ&u?#j=B?vw7sn&APxU zUw$_y|2K%WzOQujG!-Y6>e2;Z`Lq=z1PT1nkeEr<>H1-4Cr2dguq^ZITlUA_m;dME zA5e={+{8``_r!xyK83jYZUm%tFWNC@3HhmPG~pwki%ec<>0DF~O|ii0!?cw#3p=4B zbfxTLA!AXlosw{;re+rbS>E1N-F{KYh7KR@4!28~myxl8Tra6_NF;%R`N%gu{d1>( zPJbTn>(m z;8daCbAw`Ths+4TNHhpcC#FBF7B?dfjIVcAP_)n9__&kQAybmxG3*l{JN`+=?PRiN z^&+!#hKhT3@g0#I4_*@o2&C-+aeF`&2^^P`sqleuxuqIbz%PErb{7va8}+_!8s!}u z(|#z6Duj;vjt}UrZ6>A*HeCe|!a$Cx=yZX_M>S7AtXHQFC(k#5l?J$O1(3Ii`&13H zVLTBbFgrCr;fZO^jqp4OzrUx{jSlkkVmIkwrr_heA_yZk?hUV}-PNm zas9`3fP$V4xKH&pSHt}QesZk;)&VmkM;cW_@>z*$lbhr=^t@7mr()F=st3I zIu~lEKrAN)i9$b^&8(UhVmkY15NvkU=wz`C8w;F47wdkW5dO=H?*h?ZM|EaxVV?8? ze1w@wx5{Y!q?ytVd|y-TsbnNiwtYx;_T(vitrBVx*H0DZ876pcaYypQn}t2Gn#2w+ zNCm{h;{%>y3^H zm(B^6I`*91lWV%97>F!(R0!)|c=l8fSSffEBeCQB>~=0S>H+!+f#@yA--D@AYIFDSKimyV$^f5 z;Y`;wchyW0D_Ag|VQ^7k!k9U6v_6C((oB)9fPDj~E|vB5P(lNr+=k0Gt#bU?r>OK^ zOGZ>uw9W$Ak+L)t1v48j9bP(JtKs4_FOhM`jDbnH&&CjI#@yMs7NC#M>k;SC2Hy0l zhgy)#(9mFlotryN9kJk6laQNEi&AL`NGBUgk2~qIRRd)}Al~=JNqPf%)9@xAt5EYIDbC zOVckMIO2x&wB*1e1U~H)t;7)CF7^h@Op%}3r!z-oN0P$H0_QQ7J{6=vG{ zTW;VSMBOcKR1I#i|DeZ=EO@j5V2g&6qOBsenDo5L5*a?maXHJIl7Un}6~xP^hQ!%| zTNf240yjj(1+|%2U=H3Xi_1@{hlO9QZ+ZQc^B~%fJZ3RdD34{hDQ^(SeNNlNZ%jq0 zkCF=14MznD6P|(DIm29S`gTwvLv3%T*>es~wt*j?H&grwUmLn#+?wZj*@TVrTx?vQ zL>%1a)Q%XD5z;$RVfhI2w4vmHlenposC7P+4w1;_D~9Qv7Y3&kyVQb%sT~{U_-Xw| z^A1b~-dZ^%NlZdwi*dOqfON8*7aBrgXu zZeLZb1U;V4F$XvKir=?`jANU?$-Z^~wsGX((n7dMXbin6dGhqD&%UPhn=Ns}kZc1X z&mml>d22c6L21~p zc0>fr30kb|&j~jvla|D0JAiB=gB;m# zG$_q#yE1R$fe&Y}!b``(+ScWD-v-*rl>L#q6@lkYgO>#>{WDU}^;`|lNJx;Ab0B27 zdAw|t|9*rt|LQ>O3qt9h=g0r3C5R~b zU_!_=aD5sZ*$E@MRt_Sw`H@6YTZHYa;9D|6u(NXdi^k!bYv#_I{n~;z7I~(Bj^k_} zgU&Le(m);^5dn8t_<3v$`vU*+&IFLnJ=S|RtN-FOXWZY;HVl=H;#wtU%6pK8iq#a0 z9eKgdZsNmp;|JHs2-7AKNBHbN>>HCt-_~=8s(I08bJ?g-H8F3N z%NqHMA5Bi^uGe-I_{LpQg{aLL9A#hlJYINltCdZeSTyM|9zjoexFog5*&c)ToI`75 z%aW0@vgyNaMony2j&TO}aNyRs0A4pS{%IutF##$$GD#n6L?g~tx20)6<|Ueb4A*H} zwyy^Daaba@xXyzW3yAq03-Ted?3Z;nMbDm;ecq47grz_m)HoqB3#J7yS;gy+&fJKw z!&3{UzVuWt)iz1A*Mo}D`5Qc0p>%Oo%14ikzCs2RBQUc0wTon%@w_#nhq*5s=->SD zhlH|p_#9j?RVdrU#^b#DM!Deg{LqsJDXR?e1}k}p1_vcV`o0Tl(Bvm?F(Fs;K9jja1_k zxMY542bmpf_Q||GdrLH{-*BiV%u7KSH#(KzylrRl+}d};=1pv3#?#%NPuyDd7ZcLo zwFP*??>R;~r|?kvkFL`_nx=iYcrysk`TgP;VMDf?4yhJ)`37E8PxI%kLh02tm`@eW z7a%GR$K&?YhhWj#L&8_Oa8bFR*@awks_66{M`d!^2bC71)n&Y}GBz-9%sif_XdtHk+Wdyy^C%WPXvs{)ptCz;ZCR1f+ zSFZ|fCs*|>sMZ_rTm@3BPSe>iw<r6ijt>180MW^-ENU3(IY! zYAtTSO~=suNRAo?Z1-CqnC$pshJ_YRQ+UoJ*ecs&oDl0097MPo4^im`0dTzX2ZIKIQ``0-6dH%)LAXK-1N%cr9O&eYJ#mR zTq+s5yir||&WuACJBBYXU*bNNLaPc~UfJ#E*`UI5l5f>7IA`V_#`Gpnbo*CTuUJhW zOzu!bjaaDAI|kfx#OrZF$%)xOpgl8V=k8Qm)5CF`0Pcfm$vufg{1U)7$7i$-gIqHg zFer5q8c~jHcr6#hrLDdrAt6>c|G2wskK}^Q%OOR_rAP8_$i%oOR)SF?ve+{hBYOG~ z5JfuHoy-+igbq{nxPm9tH=iQ4$Ir@)9XZ**xixA(kCwchXg*x2c`+elrV#>do28PP zbJB17iuH8(d%C0+l(v|dShW=Urv;r9m3zl{uelhaFB{h*Bi12@#Rxy>a!gL=k8>1O z$)^<4=UX_h8<@KYrPiQ3d?4I!f)u6>b#PwNxC#~FEgM`u+P$iohbZN@1CnSAAc8Mk zQ#8!a&PR9DS+;xcyHshpga|R{gsUzz^H($sMMOlV0kd(+25e@PU&k=9(-JNgbD)d)M?2u{{pJ@jOw-^dUa2=?u<(g`wku;SfS z-Y(?by<#9ZyQ^R$+pMhbX1&>wN_?t0;#{s-HA+9P2=Yl;>V;BSN7%Ul%6Dp3l_c(P z>nXg@eHUmbWxQP|o{a;X8tQ2-XfvQmrkOZZ;f81^A+MnGhO!5x?f#0FmM_1xksna~ zRL|J!TPsOQJ)2$0Kz$r|PbUYj$6S4*+fx?!-a<5{A2oEF&%ElQB1GBLzF#6iSXL!} zew3TnyfBU{H`=`N%gm~P+{VRJ^|Yif<5>!EOWvAC3WAT`GFdXv=Lk>S$MdTuQF0SY9R)_skn_^04-f`FbU%>%KE6FVU%X zbQIiV%m?lT>nl_mkqoc(z(Q!*!3V788tqWiA|3as#i>&5qitqOyB_6 z)~pdElzn@2a=Uxz4yT;x)Sj*#w@79Ay*y$DvsWmonp=M# z*)*FIErX*QqI=@@3=B&e^PP2@a`g1q?5iLSehyc8`bM~(L(FqPkSlZ}HFJJeR=?fO zhjHcfVM(eYpSgMb=Z~$w3O_Lag1Kkbs}Sk#TTtQt6|$tok^7Z{7H(#tGS_Ii4jrS6 zsN|A1(x}SS2R&8M24i+187YF~t#b&wm`+`!L(N>WnuawjCoNYrm(`<9e>%-wkkvRc z)-SrY4hhXm5BM(|CZV#Na6BK?<+btKT$C}Pl-1K^{LVB@XMsukUIc? z+u5_}KW&F_{HnPY?EcbY6_Ci#MZ)7*q;@KU#tbeE7K`goN9Q+eHjj5EBJ2P%MqXiq zcg{CoU_meRA}L(xz_ew^+?x-U$tP4}NyHXdr+9j;f(*FetNo+c&1cf6MjH@!=1V5c zKO2AkoTDk1o#mOTlpTX7wT$Gzd|7Q`lr5QeC5B76%36i4kUM!5MAbTFT+p3gUn4HE zggK7h$@{#c9fj_SrFW^x4|7hc=VXj-skQ>#P}8cbSH3YxgeB}R`t4}esQ3)VA9$1~ z%JW0qRh!%mrnc)hr9bdKvS$(A>DyS2@qOAho>#Xw7St{JQ!&$jlF$A3@?i+5gMe_@ zK&!TrqjPCjH)HYh$T(?)XOpK=tn2x7;3-4#TUJ-J^0Mi6sQ_d*!WQ?jbb-eFu-km8 zBO&|xsEN!D7cr9t&sX*ezB7{(cW==`i^Wq1q@r;Z9@{i4Rq#O6DPo0!WH`!yxn|}m zRtqw3Vkqr2kE^8!v$n9q?3hZjl=uurM61_yKSEC$eV>w0ma?P5Ic1#mJmylehYW*x zXMA>YrslIRux`_q8-AeV#_rqcGf2WYnaf%RCJKQ5%oW5eRK{_1fb>Wd;NRnc*p9 zFm$&p?F~nS>iWYj=jP>aOcycNp^k)u`UMEJ@bJ;_f!Q}E(<@8)MVIrPpX=MEvnpTH zmZP$#AEg3*EaXS@3~y(T=o3m;c5dz29L}45V;biiX%@b6DQ1Z(E1TgmrOtGMZTG<75M55TX^w`u=m#f#?#|=?Q8r3mb_YTo{mFp z0!EfN2jRd4)bpVCzq$`jl=N3?Eiy+^dW+j7d3p)ti{?ewJ%7ZhF4|ql=yd&U|B3Kwlz=2(zwlP0a`qBs~AxG>Dj7y>s zEI2wR9`H-Gia*uZW zbGEC^PlWHd%SSkK)zjHicB~aRJ==Lc=1=vC_;9wDXn!yox-m(FzU=@};FTa5Fp38V z{1SXPQ|Q(=W&#uMs@ce8gD=q znuDh)mWt#d;o%idgKc}qf~@ipAUrEjmoTC^wE31O#f@$DC==rp_{Oy8RlhpFuL%>w zSXV`UFda!QhfeMQCe-}Bl&O3}4Qb`5lfZ4lQKCSodHq~?58RXLlJXIX4j^zcm)uCZ z^mR7cLonKbNCNqNki{>1s_~~03JqL`8K5#qx4pgn!=>OzweB4glG7Cp6Y}{+gVX1= zK6VZ9`*1|0`A%{RVQi-JL_s3xS`(?b_d z=lT>bv9(|_A_|?CSQ|3%HN$n(ao#~HF=0tBAcV;OzQ6fG0IhcnE^_Er%LQWZQB|%o ztP8qIoGlvRM)}#QyNZg-FIuL*L)s{nyOGD$7g$}4#+)mblp1e?Gz)ciqJvY^@OY9Z zu5A4ghlZZ|F4TwtUGtuy7#_ga%!3cwikFy+mn?L2?o-!e6(2?Qvp`g5W}pme+l76# zt6{e{G85FB_%Ah46@Gl85K*cD@e$+Db>}b3NQDd3m1Zm%1l^QA1eq32@7uwxC8{dl ztoQbf)eHs^^T@4ppF$Y%#$Lu~tG=kk3wf&G-O;<$DZK|ptP{}s^P&jd&ZKU?5{}E9 z&J`9on4BpAK=!C0<{*ictCg@Jv1(E@_Oe?G*E60mPBM2LBJMh_U#s%ZzKny5 zn zwi}eHK=ED>yj8#@YDk0Y2A}d1DB}54xyUH-PuzexQG%akFAV7+lB0vS0hNHAm!Z zHJ%Y4+S{q@AyD2npX=`;)9Dj6g_<<(v<~Ly+;PYw`JtN;yh~lgRhA__e?kc-4TuD>HkAV|LrUL zZCU@1<$m`^vY>oi+h0qL_{<|33ETBobciPX_Mz)@il>6!2zM-`C z08yj_$1WxAQpEncX)^o??WJy2TJ2AiD|uS0MchhirC5M)IZ*@L4Fap^=>Z54@9#el zLoYdE-6G%IW&~Ls>jZ&WxFs2`tsFJ6Ed_*y7pw4$#htYGa4vl!dVW5jA{VC1a5FJBCB=z7#=sa z3px2^!-O1M?vv`GMh-XCMJ`4>A>&sAf&J_W%64Y|1y_R0#pe)BIq9`u$=qO&ujq+U zWc~$R#(LNebTnc6PQsJ09$?w>OaTNKfoyW)|LGy13Z1PrDjzPUJNUonYuA zl`}L(aOuy-dn_DKmD1Yn!ugI+oarOAm?B#QV6LBsvFP z`q16Th5l55lw#}_HKd_N1-L{9wD)COi&bw+Vm zw@ZdwZOVE1jl6aJ(DiA@@_rVqcvKa!h83A6(SXWcHP8F$2w&ysc3yBvt6h}(8>fuC zv}ao@6=H2@imq%sTm&3aC|(JEn?G}qwUVG0-JB~~%hTUyq{wL^VP`ALa1E}MNlOEO z3YyooKOH-r=X7sAUD&y}p&Dh(itzwH&Fq-vW(=V${jR#ptyjSbqC!YFDwfM&Lj5JR3)xPqy zRs-)Tl`++vlgTGxCL(to?oz8Pcwr!|ua?-)P@=3s{(7f19a?cwz5O?Y*SlNt5E;}n zPgan6I*ipL+~zENoDDU5YHVWVWci&3eF2g+qr5#(V^@FwGCKk$UBF2+4|xE06@i7Z zg@}L%ZIvo~Ih)Ic6^njUZ?aK&w%Ysd;h*tia5tmY7yt;wYR-^$KU&SL2&2d6Inu>Q zp6XSVA2+der>T_XWkbr?eCp}RM-U_iIoT;oOX|!pg08A5dF}bYmPB@)y>TTCHa)%K z*Ozlm!#eqNs!`;b@dgw>YinoM118={^dkv5$|{+e^SMJ7HJtswZ3DB(f`7o^CD`(MkAQx*J z9gq0BF@mpT(B%L7nbrAJOkij2+%jCU&gDVYBJnM=DX%E z`8c*)zobr&t>rDTS~|1)AbWa{OOb2!ez42P*^tVCw?OL=aLC19j`Ny2vb;q2M_G)O z4W5vnhl66wuHn(G$92M$G~QEGJ+L4`*rtFO)j3-CqZc3XLo3TssF7h`?#ct&`z}_F ziiueP12^1bJ|z?HxA-ZSXJy$t&@zQUQMR?b&9gXC|MR|-4oYgFHQGc^ORKS92|~cm zsH%EY@DPap<}>_kQ-)=%j%U^WEv!m1Pz6-jPU|5fBv+cZY8X$Qup+h8^}B@b_^>JVNiR z+Y>9{kR=wdM0^OjCbdgGVu~gkB$RLqt&kv(^3H~o zXgKvf3YBDJ^y#JBWv>M5gwvgO<(H_(fn@_KHq;wuDd}p0T4^!DDktVOy@dLT65qKh@1~n_o9=0&edYibp;mUA}*Bn@sTu#cP!px^DdtYP5h%qp~K?buh zfWf4hEpP?%^!^{r5`Ti?{Ug`PAIkog_}L5+kUQKFneVe->ctsXod}U--@aG(LOAyw zRT0D?CoKX%=OAZyspsXlt_A&8?XM0P77+xu!Apnvq|d^7Zz86asRjOrwcT}h{Ig=V zb`%91(O&}BbsD%uJe}c({hEMw_Pb@HYz%)e>(vi;`@b>ixJ&FI?V}q^pFfZyJcC=a zy?*riulo68-haW^`Fr70-;okbeTSF5`g@Uob@an6r(=d>lCh3m@&K-?Im|wp|AA2I zyd80Bl``KmE8<|m=FwYq6f>Vcdd4TpVo!_5vttxq5*s=KTs@^;6^ zd>zx!t@$$X-2746CGzvyl|iXUJII)NTV(v4Rz~C=>$#JIuF>UAGuqc(mfkMR7^)qU z_kuvw`~`eMggh`l0-nRAUdf)@(WOI{5wYFfMX}?^G@STXUm5H#m@VCdsD051{l~K3 zn27eDtp3!|>&SW+y5x(1~W~41sAHEUbx_1OUP(zY#ae~hZrS&G) z1Vl=7u?@ZXg+KMj#P~sr(>`4zX&VwhZq-&%l=Xv<3IAUIe=l`teeQRd=U?-FUlh#! z#r;mrdw3~(0_d<^@ILQez`{#X1G6T1BTzg=HJFJ4ZB!E_S}`>y7W}p|{-e&4L7RoBQd} zX(HU1+cci+{ELMb=GCI-ja@xQ4JGfv<&xkcT{)==A9njE(8Hv*Otb2YADe&neEr?! z{Hwzsw}*pHxn?wf@ojond&=q2`!9Yc|CGb@^6_8P|C1Op{SiF;FRCyx9X&^iqg3HM zrwq)mgYFWdf4K3Dsn7Bhw{E&S-Ok{OjNio^kF?nXv_)o5$oIqf=f*;u`3ft%ewo|5 z2o&PchLPOcBQs`$X}zMh=gai#77BAiTM~N2IMM>*DREmBrw9Jr!k=>fb83O_i$8WM zSqSZ`V|>@7-yZ(*JK}5b_l5q&v8G&F&^g2bB<@o-LwpF$AGYi1CQ8gdOZ$~d_B&@a z5>_Bx3?HoWi$fRH@dFl_O4E>u&ucTY&FlBZm>4qo4?K6q_|qk}ML*X^%Kc?eec@sWCbi-xV!$JH}%`ueyI)6zCtgd{Tq{AZy}W}exOa7 zgpQrtA2HHqe@Z~SN6-^~wyq&vROd2Lf=$cIQ+rJe|U;S}C=&T)*G|F-wY z_hhR_u6_rUemwa5NGK-8tiM#l0EqMaha$khL;p8^zpG~dH9q%0HTNe0`Qsh^Q|bRm z9y!_b9(!N}BMi11Tu6Rv-_i8Gzk4EY>d9v^rC^v+LK*vrjxu^@HE$&2^QMh1`uHzf z`FrLmrSwK02!o%LxX!B8SkqF?$ve5UZ};iR=ROUiEmg$jtIwd)MFh+woEQG+@vm() zeq7cso^MU>iped^&P~X>#DFI&LVdQ+xh+<88mvRw3<4~}y}@-^wrY_W7_X1z%tqnn zFN2JlQlk*>x*_ZAAXLi~X*&I~yq{i&R?^1UPtrd#F|i;_5S-o-#bxszw%zKxsi%3K zuv+ejz(Ro89z2E0zi{5T#bIDHycHTI)LwaA)7B%Yh!zoTlTu-XWI!-%h$@R`W&Zu1 z{^{@N_u$^&qkjKMZ;|z8DD^`O^5m*nk1(RK9iE(za7^l#<)Yr0f@sJRo{RYhgr08a z%vIX4QhY0ly~%Xy8&ismX3%n7z%whLLRXc_ir2;n&#vKX52ZnAcb$A5GF@!sdWuf^ zpxCh-RZTzj7TzzjWlJ$)Z*HZJxyeLJy1dvQ4cs|ZXXKaT89uf+TcNg|UvDGFonrbr z@!<%!;Ub-n@gZ*EJ%6Rk;`y{s;#^d7_}P%`En<6eI+t>vKd^kMh90J*obmk8CwlzoLE>z(oxpl?q*&oNfdnq z*-d2tE6bW6mTtGMomp_QiEVx4pYK2wlVY#J(>oXM&it{>f0d*BCjkjpIsG4?53=&-=dYAKR zvC@^hA%7{R|E`jLB&@f8z_TNbT+?|bPDS%wKn?>NOE=rC^Xv-%eIT+AB+5dFcJ(;pd z@>q>kt0-W0QfU2z4HT|zA%G)^LLv$=>4a)d*lAw6TVFnonC8zW^ufW%aLLMW{>w_Q zjF2Lg>g2Rpdivyg0ivsC(=c!`pXMKtR1)MCteh+^g_`o{Ya9^kCP z`!MOmg)H)xpt4PU#gX!U}ZHd*z=7%M<)(OVfG+KGPXk%WJjiz6LKpq}}6Ze#{ z{?QhnHw;b3dw1l_=$Pqy@$>fvIN8Q6xnQRvqMC0*kU%NltXE4`P>a$$mIO21uBHXz z_8XC}4quDFsnKHMLzJ{ceW>Gh??P(h5Im)QGps5ns6d^BoGDMPv50mWDLeSa)Vgk*XvB@2lbeH zjm+-j5V6ucu0YD|8f{X!O)N6duy$u_)$%uco0QV0EktE+Pl zd68BidLogEgN(I~(;wuoUXYg?QAg58o zgiui0XA+o(eVAan^=Kb^iM0?hv>*4OT1*hX^7u_VIpfLOW()l+j``=S! z*G7Z))LggDKcZzcu>D2?>~@}lTiGZ0eq+KI3^*cqgY<7?fG7xzl9v3ZoSTn-yUzdO z;5$3xQI_5>5yRU>3}Pv*YpSY;@p)7KMdknG^#2rV`yWh}vn)Ckg_lbkpizbB#bwi2 z4i}w-siLHp-rGJFU!U7>3)KIgg#SUp|9_Yzez(-NAN%`gKR;VK9G%F*Yhk@!-Iwmw za==;Wr#CvtKtqym!Fg0;9=#B)V*9Y8=a1D{)stqk{E!h|h3F+(g*d)?{MRD%?-`-q z!Y$P9+1$D9~(LF zDiw4hJi>k0Y;hhjNe~p0kk4xJiK!85$YHKKLX@&QTRH%lS@{KHW5ui?X<#X)8U1hr z;_uf5@&|!alL$qZTd0>JD>40C1Cok)jVc%47q*udr@uh{_$*&N$D{7!)ao%v*IvFc zWRBBL8q*WMCDvtFEC^hj9$4T5ECev0=o$AQC5IKq@rPWmtQ~9>O8p? z4%6NW%iEi`JHtC4y$~-a=U6RnVWHTri~gny#0%{f5L;A?7zo6bq~4 zDs_1Q>W#Y{)17VevodNSrC3i_NaSJ~k2Pcj;Oj9B01Gw?u0Az`A2<>jz!}^u6x2r- zX^R0u*sks1NfQ3s?30YeW+fK&)l zLNAKar4xD)=>Y;ls0#CC=FFKh=Q-y*=e*zV{hsfiCx0Y6Yqz!5Ui)5q-}il8*L#^z zgu0&oawV5-zYEhwdyf3INjrj=sn>iLS?zv3;2%7-4L$Xf8SnEmb5RF?`c)(RvD~bh0@=Q zrTVuW3}0vxc>wi`CoQE2QTN0>`shkKZfED}@!=G2eqpu5poord4{hhw>hpd$i_ab$ z)$8*og%tK$4S5t;<@ap%wkp|J>gZ_-{S1Zbw8?|{G6=(7FP*OT!)zUxx2e1zKlt7S z3#K;gKvLd$sw*3mXx<*A@tligHf%$p-9ZevR)+ z?emLHFyYnxl$4x(I{;=7+*}hgN`$6yT1CL zYf1($^`tJ;7jTr0%dpBr;3jotvQ}w$6_B(7TchRfL&AAhck27%6&Nx*6yI8Ul5C_- zDZd;lqOPCD7B^m4X0g2XKyHU}hPV8@-?#}RC9{dCYE@HsVbF#z@4y@e zs{(vzw(}oT?s*GhLlnmp%k)~ug9{Jygh+i3ciU=h4NHb65M>kv5l_kk@QS~9lC(0kd|#@1UOq!k zt<=G7V49sithXNOcX(>D357}{W%zGoc7I<-fkXW~>Y_ono_NT=H(zwkD)4IuI?m^y zP&*>h@j-)~+1h1xpJ;YcGOU7eLSERL4-?b25s`awZimkj^ZobYOb3U_4?(b#&9yO1 zxq(XVDt4kyuwJyz;z)sX9?1g(`@YZ?>Z_+BmhaGLp958>Vbk}{y(|9dpO1k*`;h;M za^&CbSjbvTOWWXs_`;_f=KOsTCxpp+JJ^48LQ)EryMvdaD1s#)?zpLB-!Jj`I zmh2ji25o%(du4l$|9gR~qXN*DmeNqB4PQXaYiy_EXECqVRNx9~A%qlaNZye=q{;9B zt-tWKe_)s1_Y248n4*L;giG2Zzi>cZcCJMIEnXSKAm72|rcrUa&ELyc83;37X(SAk z6@P;F*~rQucJB}@Wc$0nc54T`SDUPK= zekbX)NWi-`$ty8XOFgfR=_{nHLrd)5J6<5%9T@$je{jhaTWU>+qFe3Qm*fnsjBnG( zlr}c8(dYT&p~x*vVd*Om3VlR9^$O2IbH90KiWltJ?)>yybQ7%N$SdJf(9WbUh`frP zS|<}Iw`*iu3KW?r^=iTC(CTleP>v-6We&y(p1e|1r3326)cr$*s_V9BumxfiImUkK ztp8pKGBDf#;@1<%Xw&Zt8o#UJ86*23ebx6spXV(iO~7_^lb+ZLkhpPC^oh9U_2^`s zZ}+qEc=(3snP?A|FgCyj zPr4e9C*&Bm>3y6qJ#)*|x0VmfrMhMsz=W&LpIRHJdiL%4vj*SEwJm|F=VJkG} zo9qH4+Ar+#*kPtHI=VD2O}P`s9LOr!F4&!(fohO{Q9H!wJ3Z0qjmow)V;r1I)zANe zj9QLtU;%+4V*nNh^w{Sh?>Y<5#`CB=S_j;{&e#B??|i2M!Uar(_w(75{+vW^hd05K zzwvo5ooZ2}+Wnzt6G!0qcL$?J~25;To&f!}qs^-Gs$Xof20j3h#;DvVVp!*)m z0@7;u%4%qAIAyz14C=6|9+Bd7J%bT`!10!is4)4e?@(Zgj-~PVcSZp*Q2r=VFEOfO zGY%bIM0YRX^C%g(YDUb9v=PnrrSPCI4*@tivj@W*-(AY}m+kjADMOqBd1cIqr@Er9 zE()_H11C;ZU^#Dp;y{nHZuHV}rEZY|ILQgUWO@?+@v+%>Fc@;B!1%yQA@c1&;HX#l zuy;?|jlN7!`iiq-CDgts7V5|#*I!pRjks*v#dkoG(e!j4Bi(!{Jrez@U4`H%%sYL9 zL+*zULqDQ<*(i`4+syBcFPZ1&pCczQXIgNgRotUDD@(%)Xz&t^8?hH;Xec32GX7>7Z zCe*k2(|b*GkNKN`P}>1q;SKJL{h$7mX8x0H{^xujgS5$PX5J=%@+reZqL#XUHb9O9 zb^893lJK75$y0Co@=6+U!pG?4b0fw!26yWmH-reTYT3 zom?S>kC?s;Oc`coRN8XY2Ib{OE{Xsju%KRF*=|(@QTw_hK9PmA{uL(=71PqHe!GRM zTxAy!izPL~;7Pm2Yh)%Em6#pjK@$+Jv4XUkKuSHeub%_jh*`9316LgVEn!pUCqW5I zW{--^$K5eO{1pOq=-R28IR%m*GaQzQHr5tRyLEvtE$w zVf*Q&k>J;i;9mbtF4Okd6(@n71$5?BIG@(;O;6kE`&JH45S{>w{$7X62xa~~189OS z8<~%7{7mV|yw1bv0739`k%5i9~A9I+>OWIVYoa4WoT1kso0S*JHl>`h7!a-7kV#xN~) zU}{h(+gDubdUKvrh)PtlsvSH-eO(F7b7)-nkR4X+#cY>F70)zh8VAppIIQR<+u0@> znRmA%qdsk|)5fLIs;;JY_sE(dkaK zw!_8XG|b0@G@WN^!sFZRRn;|Va)b9TSkBEKR?Ay?W5c2qj~gjUK4VkejVki+6yGH! zOK4Cqn;%={hi=0Hui=+Em_}6ilEdn^e8R%Sq}1D+fhdZHt)2bKA#cldipH7M*JHa#(@iM17^Nn;pUZa6eOgpz-Q&x~g5QN! zbcLxT#&Kv>A1@j-xAsb=gemlj4`dg=|E|coKbA;FBVnoJLi>mO(H+lE5TuwIE|ufd zLA(8i$ve*io}r_nTq4I$MlnxDhm+w@wv^8^oWx0dJpFv|q`^BsyhCGZ#;Y$PU&3`v zhifM?aN9iRqdbytFB)yg@x~f?!zPIUjL^yc8!BSfx@)?XTn}afVL3q>#G~&O#sZ;* zSN-2OA-!yUH|6ZAGO4CucREi2NY77#XQ75mA%aM(0BP<`GJfj0)L*^X5<1_VF8ziP z?NG#`aDL8S6u)XhB8C*x+W=EgfksvDyL_j{Zt(xH`B9X^A{I*a4rBl{%Hylt1=8jr zAM>$G%~Bm<#&@|CW5{IO6z3_M#m=VwYYa_sG9}4PA~vztlAc?D;d^o37_QhEg$0me zgjcjrB0H*#a#w+UdamUS!#29D&`9$Glfs3?mBF|hORx*qzPj|;UNUeWx^i`3=yG+3 zqGQBxmRL-fVzNe1`_Iq@iJ+b76wVSP5CuT;Vh5Fj$owN}_mXhDhI7UGSygkZ1O@z- zK*#3nnR*Mbh1)JoTBr?N7Ks#f^xUGL(lXQ5%T|(PU?xy!UPwO36Gi~^W%|#vEQ@meSzpSoq-1m;A;&X&h5r!w`BCEs zmiFMKLAi#S>L0KX!9Om=EhSGmI{ejS{u4|8?~2&Lh5n8$@Uj`2odtY=_b!gR_vpW* zbm%K&M^jh5#WUb?u zo%e;3uwTu_}M|tz=#u0LTgN?qa zy&xk|!B{*f1Y1Rik!87BGPu-!8as51WVgBMHS*lF#}RpjC=sP%*?O8%f7%2MbTmR= zm{+qMn_f!pb>gk>T^$C2N}!8N1+B~k+C>PD<<~_3lk#R(hpl#q26aBVa5($c4zgz% znV8!8O6$%at^d=)KfH$i`*gX>!DoNrhzQ07$PDO;rs^b?so-Y0Z08y#JtJ0o?U=Nl zk^If_i%-UTA*~ww5;XgIV$7zRdF|Hd{v32N)Wqn=Nk=vfTPK_R^1FYN5Ob^GoH!rU zq*D+V@WBh=?MJ=ilA&4XR`b%XPr#Fbr&&_@G2kM<4vfECq?W~HzSnJ*A-QFJf!C#flGJdkv0v8E@^mP!LKxdZ z9Awi5_?Jv9CJ+G_&rHJ#8=#ELNonWEsL1NN8DBNV%zF&=&4cj=rZgy{^u?sWe0AgcP4oY)HX9rLzrFpNn#ljq zAc;f|mZ99bg|&BiAPy>y>)!|tn&_r^$rvwMeLGv8kt5*29|r8#g@3T`|6k?HAN||= z)b!nJ>b;U?*}&8Ccqj4VyaKb6A7*Y!j+@?iK|3I~u7P>hHT!2~91cD3Hgphk^14#r z{nyZ)))}v_Je8JgewBCT`#%9!WODNN)X})KuEWzV>+8kp20To^H<6t+KJQtiH$NS% zJ`-2chuEd*Tx%5?z}+x}m4S3?D|9|jco%q6BRp>Y+5?9)3Wv&~EA0_T#ubDR7$QA~ zvB+D9ng@{Ay3PQuY)QRdnD?N#`7;qXoEca&J| ztlbF#uXK(2ltNBIQDzfQy;`a4I~8W^9Cbfq{3buVEZWecxQPL;C=N%vJ17RP6J-6~ zr0R-%VRN2wY1!m1kBp7ON<+4~t6J~A4jB;et*n?8`q?>GXTEdkA}C{2x6{b*56%&tA+0P=zgzee}5_a(Gp_uPVoE}pnxD4$Uh+BC&K2qJ>324HT&)J z9e2@L9ZjEo-*3|GMaPFARnvAz+a!^7FE;qSYt%r5!1(_=z4cH`<)kMsD7w`ykMr2o z=+@0~ZIQ_)#p)VT23!%%5SIJf^rSc%TP09O>Q-(Gi+?jwj|6`oGcJ@+=%ZEpQelBy{9p5C)Y%{rId{O?a8DKtv={ zeg-)ioS6LE9puh`-?V4(2Fcb)==;Q06QlZy#j&W(i5$N{KtyEc?84mPJo`Bsn~YpC zz%q8l%X+*Nn@(}0>yE|4PIa){48ybE*llVig@AfhS`E}`jsu#ul&TqDAG4D`<^>+> ze)0hNm@Uq>`Q6Lm3!Cp9=}zCxtbrX1P$%2S>e8_?AQNc}?T1E1`yC9MPM{_Fl|7Ci zJoUV{TD>n#p)9ysMybc-`lEFEhT1|wXHas(i{kd2{DD*S-ql;DWAZCoilJ(~((#?_ zY{%m@WA-&~&HVq+`3McpsXgna361-dr?RK{qkA@;UX^(~*9hxjAJcm^s=#V1qhro& z+f&PyU|<^%TdWcASxTIMcEJ)`d7|rL{lLDN$F5vV(b|1j|2m7fmz6}j(2wwA6EywJ z?EcmKvVRX}9gKEmjUBuB*M#fCum806pDwy-z^h~^@N$&=*W!NKtB-4|m0Yd60?pIT zyfoT!6}|3*2gYtwuGw@WIpB#nKLTrU&Jv$gqSNFWPbV*48Vi<3aSwu?ltNi!{_*b7 zMpqo;ay-w=JYC%E$>9zN%|C?sr>Fg+@N&A1Gf{asM>cBK!_+nqK-d8AIP3{WIJTHs zhAHLa`%tRMsF!JO+tp}a`Huat@0QPo$!G&ecZspSt=5CIM<-LYnSUJZUoL$AXR(Dp zt1x`}ue+Ss--i1qFVP$t91Mu=d$Yp8Pbi1G2{VFXj^p!LRP~IfoXE+YFTZ18HibfJT?h5~PYR;9^5nihOKJj<`*>kjR%W}4xmrJe<>Fj!C!6sYNHHRpoL8rI@6F-rFyp|19qk6 znt=YlIQc(}xPQ-1p(ImeldxGLP-6SRzckjch5ooa(Z2GDXsD*&RFP3vqQJFs+Ui77 zWLM6U=K;R`-(Fms1du-G=cXkD=47X-=x!AnL9dZkq|OX+>Xbzv5PC6YA4}JLW4(1* zUe+^Co;E9og8oHqe=inyFh74Le;C)#=Km`xh=Q|{gSGY-pe9D{R&(Ql-cRx@VE}NQ zD~%gKja!Jw95p|S@1*ojmteKb<0c%7(JE>%0M!}AcSDe7lAGjlbb7E_0iBMIqFRT- zEfc5DlYaW|&(B_a=+Az$gg-gd{+2BA$1x5=-t79Uk3U-XE_n28!b~*is<*SI>U2j} zZC}URQBK3gTa?93PX_Lbe;%>eaP{krOy#>@@DJ}l^HX{D#lBFOL#ZsM*0GRpL?bCH zbNI*+JxLR=gpb8or9!F>=u^Laxzwm+AzN%Hxr&h|k?!_uow!&2i3Kxx;#v2zN|*Du>#pzqfGTw>|L~B>2atveVo)b5*iPb& z-j6J2b{s}GaFlgea^_oVFu|xd!Emn;3+>} z2JV-sLxUov$A3nn2)gYU>o+W z=4Jj)BK?bdaI~cb8&)+i1#SVl_wb0?StFhg;tz!g8E3kRTPu0{iI~w>JCL(e%8P#|9ICgfxus z(DpX8jq2#4!&dt=Q@MTR;_G|MsOrZ!8YqC)FAh4tJ2NxRmMkFF@Nls+Gu0yKwj;@U z1EUG^dg^vdaDA)V6zfmHEKRi@`DV=zj-Lc@XrZjVE4ZFg&QT%T46o zBdymQe~IWETf5X~|Kl{SvU?1=sAqFTcTS~JifgoTu2b`JWXePyljHlJ4&dhxe&RUv z&HP0gm6{oKBn|)YQzQ?UlW06Kb&a$~Acm8%iSMMvA{R$$(QR_M*#fak;|uH^IkbL) z|8O8W!n`lCXUJ9Y_@_gcIsW=N`{_D8;8p3hbvl&mmatE~Xgn#)#B7Ya5pv2kT(?!A zPbLI*g)|O2KIUy!!6EkRZr77})r~yuJvKVSR?JMmW;Pob@{VKwycPK}mm_}oLY3F% zx%;8CSl>OvkCF_Fv0Hm-ctkhi%EQ%SYvJ0D08Q>K%bLHQ|KI#-$@#qDi>DR~25FfRBYb z4TSxkBzX4I4GaAiQU3(Zd}lDU(|3wXgN(0LuIV#3ep5Hs=qsxzAiQ+|0Yg}c=V|pG zo<=iyTKwz|Es9ULskb~WOmQtc8N?sQ^+%4~aXR!-+c0@-OSmC5G zq>M^FEZ^&tJbVDxICoOKBPp{Z<5H);R-Wyz?bNzLU~SN$+IlA9COVOV@-+KxIR#1o z4LdojrT1b1M~oyr5D}XQxg$=-nW+Pt!;Zwr#Z+d?b9S^Wugu~Gm1@;^SiWYnlUw@d zxi!+aa(g1PblgW5$o-MCa)4gCq8lfY7q|YE*_=Lc23Tww+P>+ zP*K||+jf;1_p?H@UJo14T(390YPIvmqjE&qGzfD3TLMT5!#Vzod<$Jap2wGh#@ymk z#_GKq>9J1DI*mVS3`cJs*&7)c3lM)i-=Z!X+Bxo;99oG z7=(+82Un?#$8-Epz4PSkVi=XqYX^srDAGCMfjB@>OQnvZ*HfN!Q=D=^p>5NOx@+9j zS{^B!nx-BVbWT4Pp@uevgXSRbeES0_*H@m^m8ru0P(9Q(GTtZi#3ze;WvTsozTURY zmR+t+m)FfDWl)rD!!znNeCuqoic~D{!O|0CeIrC5Qnpri_AE!Cj7&$f6-G?C`PNX zZ`!$93Y5*SCwBy7dO`j4Po8FsupY;EZ2Ffq=>xFRi{K%KQ1C2OxnsoQE6$w1XvDqw7@i=!N*+lEq3d{@Gl&bm0F7FAA~ZLpKLSDKF)pN zz#SI4U)|$WU~a$CceOu&b0P|aC=XH~eI!O@n791IL1(IeHL;~BsRtI%wvJ2qd%9dt zrz>o}2ir&f3~lo(G(1$wac{MpETra2t{#KWEJ~%;%$fpb=X7!YWGWYT)ob{npZoZl z85*3L!vlL!yq5THU`v2EMO~21QdzV3sezUH=C{i47+v*WtFv(&`s@tt3>iG$Zwg?G z@I1SiLySz+78Q$K(UV{Hm9}T2GfAC4g}w!*Y(6p@^j-88W%ODv?mn)zG&O)6ujoQW z2wXc#qS9!x&x_s%KBpbA!3{c;4P`z|2U zYIq(#aP*%>2izDLH0q*U?Rfoc(*_aZ>JkZ5*j<)WOuRxFHwN5&P~-F5dSF{KbD~f= zopW#XVA8}qF7^%vbw1W#Wkf=p;?2!__3s5n#oSjYZ)*?Fp3l$Z zHQ`@X(9KD!?YC_E?=ujOwYk}Vzkg|%-qS*NPOOSbfiO6pZEAh|>ac3LNKW48Kf%ws zABOh#whu-$ux`3@w-KPM^TeovtEK-u^}frmklO=zN;th@1Q_;t5lR9e-I*t;g!1j3l`+p_i}(XHz<@1j8RUDwt)7u>$lRsv{#Dq*s~l@Mxi%I?_c2I zR|TAVP@6~r|D_XSSxib_f>T>t$$`hqg0fMH=cpdhw*C2%9faTVR(;m%rg^nypnAN%D1W<*kMzY!jO2XVZXFve!?&w`VuJcD~@mLI}RrFmFfVsTBF?D0CF)iL0{_ zB6tY2FkGMr15qgd4Ad0!c>B^!`H2`dAw9w40a)g|RY-^wRK<$7PyPa_qftIgT>V7r z-O;+DQYcx$E^!iSN;USLZ3l>;LSSSzq9r8k%?bK*c#pehDC9|reNxh>d80>C)B7ZH z;cboUTtY7-&j>A&5r4gsZTX(YpD|IUdr`klx1a0RDQW=`X`4G|9!*Pp_Hxo zeE-kOPlQf#2UCOH8%8nRt-uO73 zraZL?^lcTH$AZD6J^pZ!&&>p=4Cx*MCvzApyLy@+3h-fm8Q*$&GMzOWyXv1{Q?MF> zv7cARM3E_-iwa3 zk?c>eLf)<|NHg-FPCgZ^Fq!eg@c33NuvKLsSAvMP2?6F~1C(Dl&WzA==bXN&ZY=t> zcx&i;$PL%w41Y$HaQ4uEpjdad7Yfh|UninV#+HZ- zF)|77QCvxrLr5oQJ@Y#hLo6Q=XZ&0oWNa9yaD3M&nh54S3h-XVG1m~32lR#ZM1sV8 z+2}rqpe=KRkH5X9Ri~-EOIk3xKF;LYqhDd0PDrB;2(HSMmqzZrghH znuHmP70X9$1EzOPK~DC4N?Uz2uaZ)E|G9L0LX{HAq2ID4A0IjF&{U4zpxcGn1DvJC zRrdct2k}3CJA7E#`4mo9bP}e}UfoG;6S@DsDt7bDu}NLebMroQC_e;hYv)nhd`XH= zY-M3i$C#>GsCT}B3}9=C`|LA1UJv##h^3iHrDaQx{LUFv>MBd__&AL3k#00&y%{~2#tI{09BG9t1*WpBK^Lt=ZVY@bsi3U zO|LrG+hZ)n4RV?gTON$W9w} zKt0;rluHiyQoBHZyyVw-jYwKdw#K>0D_j|gb>@#nNZyuI;5zuj#e9KRTX*CrjnWHD zd(3ZZecuRs57JUh-il1ZLxePo!t)D91(0L`z59L4JOlfem8%Z3uCM^uLdMR!u#xwa z%#O?(b1l;qGX6-5p_(Q5p75SZ)Y?R+>eI@VvyRkgd^RBm7Z5rzEh(;N997}F_Ui@W z19EXkk<-fX{b`Kn+7s8y+!?$*^zM%D`t4j{>e#N>4%e?dFsT}16tTiME7m1~Nv!2> z``NV25M%`)oPj7hvvN+J=~mGYVwge%>U1rgH787l_Eir%4WY8dbN2kcDSkX!=_EU9 z_r`7Do?U45e%;JR5!ypI|I7h;>B%=dge_ia8z>HXKzTf?sR(e5hh!Hdft;M4Y7Ng$ z!$3|C>HTTSAs>@dp>~i#1Ag6pzv8A@9;o8a%)kgNhnIjfX2i}0;c(cT()vzepqq_n zCwC-7^^4WQX?3Z3c4w&ajw4Kp;RS) zlUy6*mg<%nx6$%J#|nP^MA9!DcY1#(DwFVY#!@Rsl)%&Ro*gQXRPKe64awZB?9-Qi z=QLwSJq~d&QjDp|W;^wfQYu2Zwo+Vn`ol=Eg>jRn`3CCrib&#Pl9^UssCr?feAuFO zmYl{xc5B`7LcOXLytjgr$1*j6_d}sy`~`X^7oXsq3udvv)8KBgwjNMvy5w9*Es?I;(i;J-U)PS#^lwzH;A)D0?>NS(;l$OA^%YON9V6wWb zJr`vfuBg(b;`y!RJnz3W4&7$;3r7Xw*GiEz73MZm9(!)2q>QsO$;+*bTylt^nu2Hi zT$+6-FVuXxnx9GLb$n>=^8|#G@%({3mN|LB@;uNA5VYC9!QIBjYeXniQ@dOU&@RBU z7*ic8m^mU@__kCxV|QavY=D;8r#Ugwk+E4IO_qVuPuTE#s|0~nFzQ^8lTjfo<-E*A zBsq%K`G}Q*x>C_TL}cgNU#kff^~xuQqZ4c+Ct}dbl#JO09f{HF;Rbm%P#`(Rx}1wk z74BPMW^^ex2)#gaK4aS?7rIjEUJEuOsV}{JW}TGf7xJoEXnDhVPlOXX`lT8yA8_YDH8wsjBaa7^^n2Siln$eENeyqeWQ z?#J!a{<8MY;_OnZyp!6RP{j1YH5C9`@T88XOTlH|CLok&u7G79(@&2v&Q~lySux)n zrV$;cWEA2BSz5}9XX2OYtUT%DHk^o-pnRt4)S^q5Hh%MT8ZKcK2CThwj&`Xs zrAFl;mB-xpwp43-h?i&?t9=lOI2FLakAejY{7=KAckYf;x40W>TBokZjfkIeo6h&- zw0*nla86d>l~oSZuX%S&l_m9zh>t2nwx9l}*W4zGq4XBK2+}hAv3yDTwu92Rv4?TT zIp3ZsRdP92)4wfv8q9B7spwcnyZ@ohlfv|837J_wwc~3hxidfTDclIoID`oX%Z`q`NSsP++SVT`A zAUdg=Q}Mtw0BXr(@iWUqXM60vrNB?|fS1leyy11IE6Zz4Kh*K4(ylg|t82-$Luocc z)5we<`pjS|$ubqno!ERj_;KnB>IjgWkdqK~k-ekBtiPQK4p}wZ>sx82IV$~duLbyn?xW;VtCE62kB;!K22Wnwl3hZ$1oK0L zQ~Y3A2Q_p21g9Qls+${;v1B2b9~YL25b-NS2@5+zyVp&G5-rsE)P!FV=bW=g3mXVO zAGoCy#^sRhv{zQVuS81Pi>%6>5p3<~+S4}9_@=+8ZSL*t#@80oJMgv0(F?Un z5>5f8Vfu+%&7OwAtH|key~oQR({JJfEMJZmY+@GAZLQxPD;MgYw-0`(P^M`BB3_Q~ z&&YvCcQuOK?dUQ)b22>pXhwh4y)t(XbT}2eG%DYg0ah!IhpOU(&$qiREyMV$+T2%O zv0%Is-zWkqR-i7=S508BPX>7-GGfb4_z^Z#1?&BI^2=4nw)zQ&&@1)Zso30KIL046 z*&g0|^>(?<1z&AXgo8Xfone={eQ|_uEw0MR5CNrc=F*9;lCrBwnW{SWg^aCk+wvF|s{-`h)e<}=6`5*$@_c2}PX@See2=a4|?^X$g`_wNSX6|Mm zgM$VOu%je3t42lZ;c34bCQKEy22g!p=V^jpA({A=^}$9yzlXhg-xt95>^>Wm>>Q-} zfIrftcs38Y;-DAmy*>&QYNFprUTPyc*xZtsjxQq995A117Ko28*Nv|5D`ZZa7)f7= z=el@OL)N$wf`wI2$*ZHwGN(#h=@uV`?#6#~G$BUBI2=gz^d+`FF#Cl=e-XBKsZ5Es zF(T()*jHvnaVx8O_~WAoW{SD@G&@}7g@Xkv=hrTN)m%^5@L1XrEQsX+FR&VlAXvW5 z6iuKvA+XCi@*^Vmi~gdj1QZI3D|kNwj0Nmo&qYW8VJ8HVh0}TXSeN^Gx)MrkN>_c# zKJ4d?>@9w2G=N<-?qucP^Ap)!zOTw!b6MeU2;X>^_AE_}FY#VW;)8Kt5!{2t#cS1y zXr4FF>St{<0NUQCY3V^`veu$}uvC#mU6t~A#O>JB3$b(KvjTD55I|(NCc}4>2J@*EfoZ%i9lUj}O+A z?j$e8_?NrIu69m*g&u^jdWHswj?KyQg|ZcQ$dMUxzi{+DefGj_p!LcO(OMFj#&W)E zLJwI_ZDY$k=NU_M^gW(Jp~W0&{sw!4C>9{AAnBF>L)K3%qFBC&#?~H}-Fjld-N#kt z)7qMd6A7T#e<^hzzpF>YCuCm94G&Zavi>FuQH6p`Ka)f5H+lMb5h@43CW1u;05$u~ zCSg+d#C53O+d+3xPvODI(?Yr4e&uNy?J1bH%t`KfZ3Dq{8*K62mexxmB81b-SK!aSw{3D$=1gWJngXjbP;6}! z@SS(YhD2lpn`pTGP?ip{McgxK#l}=>rLh|J9+@c@8@^ciF0v@s9#?k$1*WZBqdj)n z|GucX6DCo}lY8RcJuMmXa|dw$nDc8&4kbGcYHF73$~8Q<_Jvh6rI1{E>(*Vo2Q5K% zV?F*ue|r}Ck>tK*zZ}1_rIQ5~fiDwjAQx4ssPUl{H|xH?9$crQ;V>dX8TU6F;%cd} z3k%Mv-1i}l^tHAdc23Wd4(xa&x>ri*pd%nn6A_~@ml7aXRP;x3<=ooFh1gIM`8B4j zO^KZM$X67rVV^(CjY=hFEROoHVvBbT?xd;mu9vn9kk^fr(ld5CRS7y>lt>|pLgyw34rI&FQ@r?-n-WlQeG(oS6ZQ6%3R+5{fsQT0(& z;u8oslY}p5=rdQ~tt<$pU`X9q0QsJBTpS=W?%USmyledNE+!e)U*>r}PE7YzEI;W- z!b|Lg@Q)bz4J)$G0bAWx>R7%2__+6x1=C0Va%Q)R-V@!(`49(^eet{VUhy3|@8fM# zt;li;nbMY)?%D1MR`OB8e$*o_(&|lc{4ddNMP~-$GLywUH@(Mg;CbH zH-YNswd(7iP>)jWyg+#_b|)a0`usNHVvhX{4Y*NS-TBD2pG<<&he$bI#q-ap0u`*B z`+G^xQ(5fJ;vLb6`)Czrr8Qd!Jt~-L_;S&s56ASa5m4^r8yn9HD>I+6Y4S|;~8q-W4#sI_bEP#>nr=pOVoOFAKA$*GnRB%xT!7=KBoud)2LYJLcMOZ^3)}tz{MxQ ziW7|Uy;2!;OyxXc`ci%a!tEl2=5^a@P7OgynPK8+5%@2qh=fvSX8z|cd~<;b>}TDf#a{bPAj3r z*e1$K*Ey&Kd-BQEb825jD`-I}!||H3PxeUEh5bvsgz&s|Xx*7x9SBkD=nUM>qa#ab zOk=QZ02jbcW+f>(-^*3tpd0Zz88lE-BHszq3(tv%h6HAMW>|uZaCt;*OA9VZueQBA zB7%&kk(5I?qZ+|J@JESP4JUd&>VVqg)i$OS_HI-qjFv=IRkbA81{&_~(7C|7<2uH2 zX8=Tte!^S2WavP_DkHd%KZUY8%s=}e&HKk?3(H$)`6_+qy>`Id&kFtG6jE2D@?ZCp zuG&BA8_-E_KUWrP+I3#1Bbbrv?{xdP{y-P_in6k~jPN|lr@CNo(4EoZ0 zZ_7_~ynx_K@!f$>jBQXxMb_3%-O248rvbmNu0s+N6kwZPGF!iJgpc|Ru8jhX%gAIM z_;ts}gja1iFYlnShd!P60D;*s6DWUO%MK|s{iA_nLC9#<_2?MqQPxNJK$?1O;kp7@ zlouy%zae{p-dq?HJl9dWBNCYGJotPXOK#L*vfrcf;`WX=N68p=urFe2dm6nKH?o-2 zmOR4qCT~8}A~9jsm(<8ziDo`@u5y?6RH!S)gK%#Qu5;liJ#jEK?kaMur5SSYx}Vy9 z*r)MMM#l@Eg;~rKN6tjoc%3v|6(D!$hNjb${q&~D`5nt3HIcBK?vPbKIYiFE_6f`t@__HAD4xn<0#8U_o-VC6#`P;lFn&bfg|3E>=bf_`ljgHtcPt6S@Hu3N!=<4=ruI;5Nn!Ur!^~{J z=7jf)tp*W;PvkX|cS;_i@<&5!j8S*|nMDD~#<%o9x5NV=Y6h;l^MSmp4`6rs4l+|q z7danv=z+z>Sgl7O7wb|2Hyks#e8wE5-tI29C8@H~3?oN76vt+rjv=zLIV~Cn`c{0t z+@8_jo6ElT|FHMoaZP1w+i(=SqJw}aMMtWHCeph~6NF$Op_fs5=%I&Zmm(lY=tTmA z6q3*agyx8aCOx5t-kV4j5PajDGdkxvXJ*cv=l4F}_rAZEf3o*pb#K;Qd#(Gv?&}&l z8r?%wvES7!Hck;^$k!+>TKYFeR!rkdH@e)Zm9vRApSzX%#1IHp^eMi=Xy_E^2o6U9 zooAC=8_>Y!QM5Hf$%;{7=i=Cnd>N{h_|^=X=8RD@f~t?9(Qn%owL=EY#OJ6~jmXI$ z29Mo+^b$uT=^D`mD{+#T>o%LUpN>XbD|_kDla zpvLDaITeO4hu)<2sci3cTOMlXW`0?qy4kI&=0?121jli{@}whK>(&pI({4hoF1kT2 zajox3UjWhX?kTy8Ws{8ug@*;mQ4}V2&mI|`B%zbSnaY7Z-C|KEY)%+E4uz4W9Mc0A zVf7)}YlXAX*Nj3@A#R#XPP1PC%jv}*L_W4wH$GI~r`iD&aX)QnDrP_&)LV(fzh$g2 zDkEB2-kRhmw_484;?~66^lL1-m$DJ>qV;&?yaGI@>SYx#gQV5TOortRXR+24N3mvG z;puX(x5|R@AB{U)-n&?L&a|WnI3z?2?T*$u6Fhj9YH#{Nxn}Sw=~|OeZQBgSybbgk zK|d!7^LX|jNGPz>5p~rHu%f(D@vS?jt1pM0=!TTznh<^}g0^VWJPm!S25h-)#o+Ct z6uDpe3t&loJGyYqwplj6YpKvc z?GiUJ643YX^_u;%($+C2wQj4(mun$=v%k}o zIBLP?EfAu9dD2=b8o}$>qa9@bv0FG1uKfkz=CZTWCXg?9sb1Bk<_o}@>v3#_I;^io z#`k9Vokzu#-ZA`|$!*p;{Ll63B#$KN_#op9A8H2$+}cDjIXn7FVY#|7)4K=l<~3Ai z!(6Gol-q)2>1~~lHvx((dSV{e=(u-RDkZq_$XRycCczO+WRuEhP@hWfWrDEFFk)_S z%f~`cw=;DZR5eSP)xUix&!-43j>bn+wMJM~N2a_3hp2|~T)IO()DawArAl{4%~Nvx z#?y%q{V#wC6t8_yX3APeWu*vBuhg*pN@7ug8TU*V^Yd7@*BKllE%mwGrCb)`?Z9Ef z@ZuZ?Z~qGMy%pZ=PbMsu(oxpkr10f5xERc^v^>Zl(LKl69hE`DWgH2n=>CNJ{5AKvbRqZd)6uYZa*0>RStz=lBaw8IrCsBA>ubHP+rERsEY|Sq2 z>%JTLy*she(GNcGG_T}*0o)S%>Ho_FyWdFdecbx?yi!3C{VnA;=iJPEu;tKZp1C3* zuN`dgUD`_3$)?p^7KN(F!?*4WPsJ66j8j4(VROpg9>Z1g?_6K)MdY}x=GeY8o5~A2 zV1^lrmodqM66?q+0`Sz0B5GMQv~2gBXNj=@^onYvoC4<*-Wmyl!gAu+8MHEeY_dE@ zXP%&Ipn0E+?+yG6;ODYlQ}2t#;g?G%LDQa(X|RF{s$*R4z17B;kZuO;NA^*No|+#v zZ0B&F#T0a@Wn1F?<{#W1c?nptwMzy|+##!(p5{q(u2`o-Ln~>X^)_IJE|boImZ6Af zm>1;EGDL5Q%#r46k`)21a#mtq>fiD8MaS#<%b15hBcD+{+YDtDgR~t~*X(6)<%~Wk zRuaes^}q)SL>QffsraYjGTDa=7O6@k4c$!aBTV(Xo)7d^&%J0*-m)8^vHkSaA z^2-LD=NxjpQ5ZXq<{Y@ae7^Uk>c9$NxjFb9!TS{r;b}{h8ulPeoMvqI3%k<94vCU^ zflkG`4g>cpozpF#IY$P>o|hD7)9Ehd=m^+in}0jaNaU5}0*+VWgrH}I zk$XR=7}J5!DNh9kz|v>?PRl~AXxhBQ0E9-pODSh-lKK^WO^g`Lr>(%&Ff}vjxz%R{ zqm$Mm4>d<)sjfM6BSbI9+bZ99pvu{1Ra zQAqYuoKr-F4zT=LdaNmdJx#QP0T)JPIQ@IT%5tWLHhwXu`u83cXdGp)B6Yj_JmEiyLJDF$;pMLY+^X z%6DY3FAC@JGBU@BCkJzgs5q#&nbYm1N80+Wc<50>pjij?*=p0=hA- z%P^qVUVOFhb(COQQR6Wo8`+77{iEzdUVZh%r@H_X5!guiEr>A#0NvH~Zq^>LDnro2 z?cDGVMp&0v%($769BtF!sU<{=iAo9Y6Qu?kD4J%R|CoQR>HOe_lFnI}o05li?RXHz z^+b%C@=S-(rA&5sQN^sb@L1vSI{5+UR-f?3Z4vD3PdDJ?1(4Ab*Gc^$e{pCq@P)@V z7g!GJG~hNa+@yb+zaiqO?htJ@c9wVi)q`vOtUab~M*1_4taf}ZY&`O2jZ``^sZ~Yk zN2`5)taPXRSk=kAnwhE$uEiAv#CBq$iHB&Q#*q7Fk00*sUUO_nlEmB#YA_LM=H?+7 zRnL>lO>gzxpfcw>Z+GTjosG1}VCH8(65kfyVk7U1(Q7C0N>1UMXEUYbB;T zZ@AH+Ix4C)J+Vx5$Q#l=%;~IVgr9~?Von4JJCuSd6 zpM%z48ISy^2|AV@#`)TLozuwb)oqI)7z&d;neFNZZx&rLDU*MG4%F8IbL0`^atV*_ zw{EpLbK19p7r2Ilw~unXdOMs8JKHA#*-LThkI^Vs=(V1^k_`?iy{gLD1ztR@l43b0 zAvkRQw0dta#uj$h zV-7KNbzc0Ut8>?GUAEmrm|$|lX+TeA(= zYg(ZXc4$0}Tk7SG-1+gY^ri9m=W1*FXs2deT-HGn_Yz(hwmWZ$vSkG7xH3rRIja(5 zWIQrJ!RVt5K{~Z;0*NMHd&uabdg0T=~B(DNO`MQ1QYl>e#Uug@`fTbD;u1JBc1f2%23@Q!^0eL@@2 zP(|EpBna8KK6jEmez0;>#UZ2m#?yJ-Co*l1mO|(7P}WP~MA-85;`6KsgMKE{-HbV{ zmyv;7x2pTb;&A!1KcC-~3UtWsI@4Umg6&Cok8FWWa!!K!YKtdT?uh*&tSq-1Nonm9 zh_j}A0fdI*C?n4sKq5H9C|o8iNmF?+&AYMd9Vu?Uo12A!li%eT{E_De=k1qH+uoW* z7I568h#b60UMbLOu*q9SSsb!#7w@u*bxD2PV&GO}xo~E|D-&T-mOVQPN1xlx6pmHa zut%=m*s=@C;u|D-r&COTt2mPGy_)$&oYne}@EO_mfwPm|&L{+~2609^#7|M4S7=ry zLqvehys|@}Njl`X3$IxRG=9v#AZwr|WQR+hv1V88*hYsX%5>mm5vK?BFI zPj`bTNwKF+`V}6^rC+#}rs0QO@gqAYrp2wueE~3YoNe`PS?_)4UorjS)+`Xm69%WY z)O8H4)>NR!Th*wts8p2K|^YX2dK(Af7+bm(ldD;dGB(2#u zE?C$0HbvW+^(?K>AZ*Pm{4SfiFTlW$8cu&U%)-LLO6XuwRE<4XIAfqrmzje~yprzO z_!rUiOiZOb>GcIX`ab2|jjk^{zgag&{bO!hdSp*Owh z&|z{jw3mb^Hgis-TWLMhcvoYr;m5n$SoP2#yDBB<(UE;>{qLeH=A%V!x{y^f6~+km z>WSG)F8<(H*eIAI#yTr^%Z=~apf5xgu_I##=?T)P&QlY~)Nf8CDe{hjO=+OB({1-7 z0ZD=TN||qSTKM{E7SkOd3>^C7zUmUFZ3=D@s#Sg|vI1i1IXgPl-haW=IZ9w!A4)Z! zDy3eNv(T%5iJY{ix*#rh-)4ti#v?~?#&*8w?(H!L<*R_P74y;d#8JbP1W}FPpJYF37N_h2b%eO2^CLBGAAQnsvFXM9zlyyKpJu#!oiQ=v$Lgz&kVlzGA3g% zlOQ{5h0$wWsrz&E`$)#RL9fvzJ+hG8`!}FAS|m+P4Zls-^uWS&BNd?C+L?I2h|WSY z9#do#TWpwU+)Ch(pVw2;Jd;Hyf^&DWJH);KI5Sl#Y8fq~!V9ctAb}+~gbvo>Zc9xg zp(aA}Ml&vh2%1bxOhI0Lr49$5iM}aq0>>F1k)F@JK^T4Aikeh({^{t!W-XVRFfkrk z2FBu+*+>#HK5?M7Sa)KC0rEh3ns^P3X4;Q9Y+$qT>xsx0rG1Ihqu3XHz=Lp4`7P#y&a&)g#n%$vb<;t`@G3N0mEGYq+A5$>7^wi|G$yrN5 z$u|T5-#V8fb+PyDnWwp!a#^w?oYpXF9aj*XldAz+npPNWJNZaBkxQ8~^p(03dYqk~ zIfCb@2T+JZ;*klkHYEYz!ia*9f;w{{Q<|tD8aEpi<{!F73`m-AdsRnd9n#av3h{f` z_aL~W1vK67JQ&4)MI`mpz}_M@0VGeqBcwZC>2WBHux1Y;+p{0&f##LG1>Q8d$!!*5 z%<%xa(^#2V{BgYfu1nR4QmEl9u!`;i1(Iaku}#ZE(eg509Z4cRoe&vSY*ta2{b!wk z{QSo;sX$mj8ru&=d@-_uEChzTSIFy!2kU(9jU#}H2h@FFVCJV~DEhZ=Cg&pqEkMglo)~fRJ zaW)G!7rp?>KMIUF@nWKFQw49-dm7g;1Oe^EES&4#kJasi}+V&)b zaWH5=KbEc|Wy2U$Iu}p1@G#39;EonO@mLfe_f(G;xVo&uueW4z`z>3paY)uBh9z#G z0W~VTJT!8&#?E~ts@9Pe+~PV>QNm%nx<|*ae8yb<0?$4J^KcT zGRdQF8WCd_k>ICL{yd*&=umr@PK5C%EMo1wp9cOmHvi=oOK7F>dag%Zjpa0|eBLq| zYU$YX3fv`We!EmA(Vx7w2+zqJ@>e>F^na4PHn@;DU&SJVZ$YhXz0@dtICE`QHqe;r;`L2b{HV_g=zgUx z=i9Mq8Fp~5}=?b4>$}1Zw%?iMX?zldp|Ua^N*@+7uQ+w&(ncZ&|;v zgDw{o*p{b;Z^|8i>H}oXA6+v>@Kxs;b4DJOO>;*-iZ+G8@_kM~-f=bztyu=J6E>Pv zp!_nW#a*B`!B@zp8zf!f^A#3Oi&o%mP<+fG^<(A}e)9+uzx+s&BI&?bO+1GIz(`m9Be`@(8>+iJ|)!lc)ZPV@5u+K^Ang8Hb=KKf5LLw z-gc-JtbEBenOi=(S?85bL622_GlZ&mEvYm&*!QNskzUK2xN{j3qcI7Px>W=$045te zHz!AM8$zKdP&pw4kQ{A5(3(yOQGK{PjbOq-$~oB44z|rVD`s#}$kfM=gNkc)?fpA4 zPWs0M@Y)9DR6SSmDJ8snEk+QWpnc3y&>tK>JO1?336kVzF68)0`JtRPhk_Q~f{tQS z#dY>JzfFloZ@-xjN?C^#-lMZh35ku&6bBuO$_EsmzjqRF)jkMq7O;;Z@)((%9P1_% zB9fZ~VgzCp0Z(V2O`a(Cs>(Vxo&HYk8nwOWrb_$=0WSXO2y^_yB}Zi=O2Q~zu4G#d zh;R*&v!k0TeEYu=Ly?pB)lc~>Z29{pK)9BPX6lywF zXZnGoL1qz>BdT;ALwz@gnlw@#Xu?%2bA1xRK{Ew~%Z-yoHq!HI`}?%kP2&aLgYE>r z)u@;A<~g+A>Rd1ut;?CAG2J1^>K`|T9kf|-+5=sw>^9_yDM=fy}U=;$Pg*1mlAhc07PSfCu>5jkGH!8%b)O$ z&L1%<_c(t#%|5cl2x>U*j!oo*802V0o2E}mXszJvmF0IGyRN=HJ+G9I;Dpy`n7Bc& zKSD#e=U><#`T$th-=}FUQd)Ro3@m3`?9%NVgF zwK*ZfK8B%g`F{7vZM)#n^ zD)s8y5t_Dw*T@hY6&nBO`L+qpN^WeNT!~gLV(!|Q>NReCHWp`AiOmF3S~r3y)c`Qhqvch*?0u}lFE8+rE0%wr-IdeNpSfVbL}$z`_Q-~C zqNQ!O#`weKOe^6$4n4xx@v0Eg>a;26?_VvF8P@K)1p+We*4Z%^$@9uG4?A^JyPA&~ z3tquDz-@MG+ElV^lJ~l1<*pS7y!7n{>SVoG3GQ=O-Uypo-imN4L^3!V$-u=^E|JMvmrv3a8XIf>rl0?#cT1H3j z!-<~r~7IiFmyz8Y)EY;n6=#MioI3_~dIpw{YQp~S>PH4RjV zp1o0J)M7%G2*W|=H{8Pa1)zSbH(2LAi_5!L#Z3cYx&6XL%i&eJgd|~_4 zZ}N%){(1t9>=2(%P4JQ->LQX^424-JdSL=a4EH(TEe7q~$<8-9s1z&EAfIKt))Xp0 z?h^{}*5dH$*(tgD2C_&OCV7Vf|L7d#>wYJ+u{*6@25iOUS4&-?iNSC5c0l{Wu2Y)Z zV#WgG*Dlo9=ZTj`>+4u)vtBp=mL!_oU?@~<0+(&}XRFWfFj0p|q`|f1ZN;;MYnn2z z4U9Xw$ULHj__L9WP^0D4+RRS&!kOl1)JSxsIhK10sK6c3Uw-^`U00~uBauWJNiTS# z=F?hDW&UI?x&!S?#sbCGywz_S@J@Dlkd2i4JPyLem5+q*wfk2IY>PZfML5UvHyh_Q zMG}oi`}BM!5r|vZ12UP`Iw=|2VQPLXuj*M?D2w9@nbxI*{wnJ(tL#XbycpB-)9~=9 z$2i9)YS+^Q3&}LAwHAG5F3#BrV|NsVMP7kDfOPM~-l^I2snQn5+2wwDYItdgwYfeC z)YG}Jg6`ngb90T3W5KM|;YcTiHXZvBqd%bu_OG7WqJyAmByD>Z4&|Yg$^(LZA7K3@ zACxo$s06&z7r;E{Pcn~+aUak?j8lJ1rl!Lj5G8b&m&Cq&@m`rTXT8%gyqI|zXNsLk z5qO{>GBR=y=VN~vz0(?|+aoh?oxHUuH{Q89pOqM&CY#n>`h&jf{QwM>|9N1ShfH(* z17vd3DzCqayAUc$!+LEGrQ57r+LjOMnOU;LL*-E@AvJ?j`}C*IzA9MxptQ*Ixj5#G zeE0{IY9U{Q;XJq$Qhi0xH(sA>lcLee*^OGBp4ODA9BVTP>Y_Vee^={nMtYqdttaUH zRzLmR=ODfAVF_lw=opstWP!Xna0VYLZuV`M6^YGvVDY?-I&&RyE6hhe;IewZ2}M;-@{N|*n%TEy$0 zH7Rmwn)y~$>|=)HRIQ!XF0My-OPG_HrdNJmE*h4Tz7`Z@zdUMLTYf;QCqq)IR7BKo zLc8(&lpK<)8nZwNK4*!o7}*QG?-=LYjpDOv80(ayq@+a4EOI#GOPnh(c++mZo*K^{ zzURj%pi3^Y8GQaZ{l}M9H8~B%G*&$L!||1Cq4M5q**a>EY*ohG)J%~1(K?a!U8CZZ znZ;`!G>@v@O#vMtpJ=Ez*sLaF0 z7`Q@N3hRCm6%*>U-NJ_q=!t%pPv`grz&$uh@Lasd?x#5)lyRb7T6@p`A&*IwD+B5ME@vj zHUr0C^KKF?j#N@dS8KAKiHnW4Tu1OS7}rKT@Nf1;FKgGAL*56MmZM2}#tuQCuHB$q zkq_iGa$;}))@hj-h<8Ud3w$U{fA~e?9J3M_jN&V?NbYH5z2gF(fQc_%q3jCwhiPMK z@fQppbdJ;W=T^EF5pZ(qNfBk_X|Qogh@mOJH3Oat_tG4TjA9^_&LlEtn~luN|CCRP zkg5`yU_{Q=->0Ro_LL zdl_yMY^>lV_ML=NLkFw8o*)M>y>T(EOH9TjW1m|-ijWweUD}}%8@&WmDqSVkbBl)=emY?n zq)e!=JZFRv|A5yD8!ch1^N~{qIx0C~F@l<0w$Yew85T~e9xt!=Mc2L@G`V2OcA0b` zR>hB;16HY>9Te>ss7-44G;%*XTK^q{vH(Z4(P%9qdvS~H}J|+ z&SB~O0tjh&o(&3{bJ)4GAymK5X9Y;#JSb67zI*sv+d+WyYP^JDPs5$4u=c=Tn|@Nb zZ~T8C^HIS@3>Muppkr6sUQY z-sYJvfEVBTb?PUk`Uz;&(lHw^kL^?6Zejx@{R`NpCPGv&PSFlMz=M5!kd*FUj}jL3?a z_{7)ex^LbnYU%_xiv|r2%+Tmg=BdkYl=YvX&#jA$4m8?XSjl|s9K07D^;{k)4N#Fd5T#t2o|iF`IUVeRO?Xb8w04PSIS0 z9r~v4LEcv)mYe*MA9B0}87N+@wM{)xc84O4UTQ{0Kg)bb}nB_9?`+xoaQpR`8(W0P{}>y{MD2|XcIWreyB;x?bz zoE7qQzB{}>%?pi*iVNZoozIR=!;mb+t?`+5^^#VAKn%)Pg;`pnu2Md=-Zt%0_~Yj# zgmd1VzLha94--0NCoRb={HMH;TuBmc33?K)>3wp`hYp+c13mh$FT!$t+%R~w54Zq_ zp)%NWG@$hb0cHOAcpvnmax;Q>ML7l1BoO%>C*T{arl zPLtg<8l>6b%X&Qc6!fOWj6Xq}Ta%me*{1fcWs|iEXlDkxDVVb2#NO&WnaZIUWBf`~ z624l@J%H#H!|iPX0135kS`d40)BF{jjl0wjO`bRb$8W~Ti^Lfn9Z*MuXqsVCvxN#c)%f(x`NYoqr53)H^gla z!q_z7q(k{Pyq(;OGMqNk&h&!iXAW}Kz%Q6$pb zITODMy%OVAD%2$@hBK1EQIP_lG|FglNk|njcLMfL%|tBv`XMNRcBqOEIoa@KD;f+P zz$@teKH~Lb;|f5ssSnTION+M!GI{oF43hG}9280$ehbYJiLaPsZ?*IqqqH4Rr^7q^ zfi)#&WFL4}US(u-`;B^)K^(;b=&5IWF0MwU^FE+wJu%B2-zmq%mwTg|D6OSpbg75j zFopXhm-?7e5~Nj8Xr%2(6(=`3=jNNv=HC0@Gq=b9u0ZSDbNrvDaI}8XJqvL9!KpbR z$PP;*hRDTrf(4XJWBo;HDydWNBwr!)KkpNva&j11kSF@#g zHcN?NA;(3g&m+}O;mf>8GLw`N>m@%7{>LChcMP)4O^FjzwS>S!o7NY>3LPvQl@<&F zT-*#kk<36);O!DRsOqMTgXB}*7L{=|_(vMn3UHsZ4VU~H4G!G8QgYK>>y0fu-tkEC zo(=?{>#8JKO`SjH1|@vBHArMjo)2(N2~Wo02$rMOoH$%~G)Nh0l0Q&}@3inau`$DL zW#t!Fny^g1GL6ECET_*iShOaV5}xa1U00kgz2vJoHiq=slttPBINd^<)@0tk)b4`leE~ete6YceuCgg)W;NLxE$5V&1NuMT^YMZJY+cJL z7Rq$FEDak)A)Z$bsp@%%Foe$2eMWz1{JpL6zap{wy}W4lmXLI0boh`TNbW!uRIYU48nTu$Mk>dS`=0^nA9ZZdg-~@9*3(ZGeYk!W3Fw8Nv&nJlg~#M1QkzGT+wH?yx`xe+W!3eC9~ZA1+kgSj@#PB+Mo-gC=m}B?aQSU%2SMd~Dfooy#v zsKBjCyoyGb4e{`8JvuvoslT)&Ps9BVar#hI*>tl%)THV;xHna-dz0X+bfvWXczoGG z|I9jt50LSZxVu07o%==TD-rv*roZ>_e@ui40Qj-2JnsV3>K*se`Wtt5j+hPS0ioS1 z*5P7J(x^Fnc`LT6hlSkN>R+Nav+JRBf9-oG|L`bdu@2(UNNj=DU;4zEc_BwKYzBN( z8*)(hT8Tz%5O<9ac>G)|OKbgu*1fw@JG|(5D`c-N9WwN&^LAJUUq;qe^hmdC$Z7SG z3|Xq4!vIrJ)}$oI9=i1p8~x+N|0W^o{(avTZ_QhP{w&&YRLz4uw-K{-6+%yqh#y&t zpd%tI@F;T0&N~%Dv#~j_dE2BnD|Z+TgZ@})E`*FXqImAUMNq6_nbN07-bd-zOF`@I zjf>`yGf8Q_R+{l^kwo(*RjxVgbUGC>-u9NA9ZqRDt4~5)T&Sl(Kbga8y))6BJp@V8 z+J0*peXGsHO35PM*W5#zT%ro^tQYX*&(%6W-Z_8#7=B6pRiA?~-Wj&t)#*(D&mWfh z>wqW#;D}(}VDb7bGZ7Bv`EE_*R8vjtJvGCNK|fZ(Y~2R*JN(mc8*1@Zy<%(e-&^^! zan&W8mG_xE!}aI{7>;~aPILyd0*j?Y9xgMi7P~p#09{!YoZ&huI4VyN({`rTg!pYPus z;h?(=djQUH!KN?l^g%wSJyY1i`RmR&;XEuw;-Z-KicCTg5T(!x9Y<?%J%UkT1i&X+B&9gXCZZhb>_t{V9D{1hrjePIOKk2(Deotc@9ofs? z#z<#j4MSVNKk;opnS0S0ny0fPqj~oqvi(~pN6xp~&!5N8g``-Ii#VHCRqvK|YS(w~ z9$bFlMq*RC(cv#A&QdG~#3n2Xx>U;Vdi{wC!na1X6mzEN9ex#ecwa3jjP^*ERgmzqaeZF=M39p>~o^u-Sc7n`np0eQY-s7adlX z!cPZ#;@6l@5MU%BZwDxA${7W<`TLJh)8+)`{5hWJS+UY3KZW9=@*f@V{>eQ#4czL8 zAo%#K6&9VD)L%o$N!;v^FX@Cc{77{Sa-laer00GIyLt}eti;>~VJbn+KWf&AD-lL$ z%kG=HeSm&+I;n%RGR?^xuF}vAG4Jgkq7_RVG~o@g3$L)5mQc04YxC-ECjvEGAib_K zH1q|K-B@=68RTSVpj;ndw1(zL(fb!N7r*WFUm?u>d%1n+Mb&06s)a&jKf%-Yt?TDl zp))&XHB}}fd3h0qbuDx3bXzbB7TLt$md=W3A2}Je zeY-YPJCln(?~XupsdUaV^apOauf#8b)%*h+s3TOUwYbK}6YcLB827FeTbE5^+%8** z`SN&as%{F2PqNCDTN12KYCy(lpi4ou$sSp8Q(pi@64$vd6sw-UexETyE)T2m)ps$k zt|-p{{(jo_kX#kSpzMy(N$yX57=(&8ejuSW9&|p@Xh4yBIwNPET(;QSwDBIRV`r=_ zrz<4#4$8n*aHVFZUEK-!Uf;{FK##q93_y#XTwBA(wb#L)V-*2k$MFA&=c`OuA5bs( zzTuXV$Nby8;fc^_?ej|;OPlF%M`yb1$ z`uZ(rHN)K9RMgME6@>Vn<=;N~AryOwU)N2+yk4O?;dx1*)GNz4oY%}M&T=$P@UenL zbPt(3*>V}eZv}qlF>^C`h6at9ph7jKG-C|5QEVXs5OV*OdgCtuHI>?0t0P*C!pPoQH7vb<}+}Uu*o$ z{QUA6Gj{h@rAV*Z)X>4tfCFi+=C+VwqN2mgo8XmoS&Bni!sHzf{?Bg;EN@_<-bT?u z-1@e&kGHnY`+OBx`}%&3{~vXl33OKq$jwaTjEIXj)-CKJ8$KvUNVY+i{R-e8muxd` z?ff!9v@H#ipLS<>p+`|+9b%?LZ*Z@+LaT-rX~VR(q?yca$-#LH(QkHI7ZE-nY5;PS zX-u$jzWVStlfk#i<-g1|-%h2w=0eEOIYf;Dv%~ICJbx-8R5x0V6GPJ+g3xVLTkYE} z>`(DPb4^bK7CL{xJupTfFD!H>0vOAzU!J(Mb!7i(H~14M4DY|^XbD-NegSNqjIPOb z-;4A#IJSmu|CgiZ*NcDg)B3k!f))V}68qdNHoY6D*Y`r*2mIAmxem1m&1a;{_T-co zX>v(D4W!cRXI@1P!1&6vgP_~Adq(tegY7K+r%ffUsc$yNFZHF(%Q~V$A6}Ik`L#2@ z^UKs?rfuMb%8Vomw&*LJQLsh)f(sU&1$c)v4Ttnv6D%@{_z7JyBziD|CJXdfC6qji zr289On7-)Kg~+7g-6RJK*7Z{ldj@q|ecDpWN&|0;_I=kK{o#Gn(Y+~EXg^l7sUOoKGW(iO(%l~+t@h6V5scGvUZSFA_`(9Hw5W)D` z$Dkh*W$4JY*;hPWHC%;LJMKn-rEVUY)=O;N+agt2W`=7qmojdOKwGWtn#zwZ-mAX* zJNNnZ@^6g|jlZ=*{6QJOKGpA1<=4J1?Du{D;CY{n8i-BUdU(iFl)3RGE~ zWi0rV8i|rMF`*AsrcytHH=93Q_4zuqT4X-ud`Mb;lIrqNdst$0pC6&%{;)dO!f?%A zMH!T8LXtA4LZKVaM)7Z3U1(F4)8IN7Wmu_$L*7Kes_kC_V&1YKO*xf_dK5BM%O~R`_ZcuEFq4-{wx;yeO z48T?wwuh~*&zoC1&k4A`he1}9(~6!*r2E0chov;mu&qC+4 zxN$1VDd}h2BICUtF}nIkFUz;IBVr;vUULKtlHIaN3pk#2Bmwh2K*`ar<_G;04@=qI z0vGy|E8>;5R$l9gpt3O3Zz?;`CB;8EH%S)%!#&0kLV+%*h6RIm&p{8 zFreXDSE2)}BwVw>=2z38h6JhZ0QU1WCG6ZJGj9CDXjY!K8@YEGJ(z8*9Vx zv0Qh^F8?sL70KI9TL%s*xZ*7 za6UWA7-;Su{Ct1Wjo{ESv^aLHCP4+_(Q$NqgeA65D^!_^(O7k*{0X z`;9R*fMfaBEPwj@Pfzu~2m=4sz_RZGRSkOR`8v>`4=4JkHUD#lBj;mWL~D#VBcq`# zE?kOasaBG)!FJruQ6}n6qAGSr0Jc0o^%}W^@1OJ8y>am0yNN>)Wt_C^KIq3q>;4;k z#{&E(Tuj!0nIRFK>*yx8V^{{mo3qvEd!mMo^AC0F>fM!Dj#O%k-x+jGG~D03yJncc z=}`%c=w4*Log3tD7dgyiol);DxH;SKcg@j%zK?(B%Ky&y7w~n@^=+a4-J16A^ox<_ zpZ?{4;yU}KS|;uO@m_kO4RT2Xq=`jZdYN&CXZB6>BEq(H9QP8w08Xs#lugq61m?Gz z_lJG{NZLJB_HHzoOQ^7@DaBZKkEdR5Q)uRv+|U0;C-<|IL+9OGP`IqU#S$P+NX>;G z_OhVr83-$=a{(Dyu>*}>KUy92mSYAsOnqVfYRe^VQCsK=`qu96SN>N7od0Vrzvno$ z`RAM27h7d{r^F0Td|sygS{t+-!Nti-F@J|gvao|0LN3`7t6Hle4&d8!qF>ys+*)D@#{B=U09Lb$GbYy9x&J2pfKmNT zqn8zp{o!l$pk#^$6y!%TxO}egmnZm7)|{dSwv{Yx(rlTOu3{^*mPAM3}pXB4q0;hvbkc7dn&Nra zY@=P-j^Oux?v;C9_r_Oy4kGd5zYes&a{g^s_@5ed|FE$?v(f)y;lGX+VyKie*BCS` zROV%rci?V2o|^UW`uyCkui&UbiIw7eUWfZa%X@H-Flt9-PM+3`i>rz_MW%6HlQ(Qh z7-5KX@(GEL>wC}kg9c@(Og4WP@c!-}cJObBEY{c~Ed9D2;D^m3w3OM!95`b`BBqg# zJC;P0h4lq6#O6&jB@Qm-q%INX-Kk)^S$V2f@ zv)y#qTXy+of&j#B-5y8^EK~FI_`uYBay{F-v9Z( zSK<(p;i3hMQ;m?gn_rL+W%ROP*(YtFs%i-C`%W&cjV>|$t8@SA3`0(JqiRisTAK$A zW{tjJhcE&Smf1KK5W6;Tl_?5tTlqOL+EuJKB9eeWA`6QOzg+Zpt+%~5HAh@S8$JaB zdV^8UC)Y*8=9ygsbLee_n;;KX38O*>7__N{kz%rvav1(L-UcW=3d{YqpE47FD%t^dT5 z%c^)FIB1|XH__ZbK*<*_G*OfH<6~b1lN$qmFG_UNV$s+bH`;V_ocRZr=LW8nyTAY5 zeM?!f6tOM0fZ@@Q8`$jNHt38N3b*&-Vm~v(g-2t@5=nOy5`wEtKQC*#q#X|_LnYO5 zP^Wc7OH&`ODt@R%qvXqx@*iZlH-mx}H=-2Hd@Ce2e~q=>iMEGxP)B`d7L?^*!eJ%ax1(mxgFQhMeE+QmO} z$7d&k4wrHc_@ISN7mH7-!#A~};emOgh(MpX*f_G?pDO}C>nAx0GM?w+G(scllw z0{?jv_Md;h2mCzXp(my1arMiF$dzj}o|&L)a|K}sg@193{(iAvQhu{9`zMggcaIJF zbⅈ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%0DE1+1u(4H19J#u*Lt+=ZVp6~bg;P60XJW4II z>ZK(7Lt@?`A@K#C?48FaH^JbXm3v0)s+WmD!9NQ}nZE_^ee)YlZ*~*~>%l_ZIa4FM z^06a>+GrNgFA+vQ9-f(`@zC%N;YI*6_sz}G(x}Ldy(d)gE~gc*alElGc1L>W+6VgT z9`91>y%w}6JK*?LwG76q_1Q@Ge6VoebumxVIPVy+sRm~okyNSHNf!tNTe#U-gXEtV z@2E!E()2GT5C+wRghILRp~mHAn7hL~A{yDAWDygf7NcCyX*Q}e^H zxxHt+1$k&Ol@`<&tb0^Vm8!>CdaGLad0_LQB3Oq6ji$)k_?9-v)Q%WO0UKQ3w+AYI zF#NH3QnvHmt zVzz;r19S0h#pfh`%d17%n;GV|9NV;($v|18aUUo|yS_X`{$>7SNDZAzK6UG$b12UdrnAHdA&ku#>#xL8Pil>3JiYZ zz$(CC(Q`20#lk;PylxUNK;)W6_XZNjacEqWH<5mCzsC@#%SK?|HNo~xt}&)`HH*t* z0=wn{=YJp~vAGv`@-nKMYvH{w3-=AI(N?x-rVt;}=qv?kea0pKbf!k*=_7{7{1tc; zK(`xn;Nn%4v7n*Rp`Ef25W>D^h%+_l)zCR!gu0JDV@5^C-%S;jsj#PdJd0bEnJ=I6%0o5PRtV zc*sZ+MZk~Jw332C-4R{N+18lRScUnqe1W3zy*LExMoqDS^Ca0GYIT)^KG>)HKo%-K z8F6XDJt4#^_|kTNa8`i_$^8T#&oo$5hva-T!m{~=XW@wj=}ko+9M0=v7J>sayRFpp z2v-Ggawe6UmMO1G1 zAkI49QAFX3fV19K*0;qz=SLDcH`j7#=i$%Jx-*pIUt`{Ld$0Ik*sEWqb4B(v;3{02FkrrhXrB-zLK{He zQt9sDo=G4DT$Vxq`i18M3zbZTN>1zrCYcheWG+-5-3T({PN4AGOD@H4&3G63moiN4 z>k@2vX#VhtFK_J2={u6smRIWo5ywzrUEXl7g*|;w|IZ(DirkXDIrz=RC^9@y21CMI zRNNo*i*2se92|V{{KZ=9w;n*h0Hrq z80GxH3{ZP)3-+tt0ss0TyxJn@xy3vjKR%NiuheabkJprtYw)zMckx)@f2ypF+Wyxo zg#QMt{Z%uqJ2!Yl-e?5cZ=rE%;1(tSIQ8c3Rf^iNnGa?FNzDvy`{lUvv0}l1p$tx* z=vASf_#7T*kcb$%_M_8^q$T+3P97@Jscndgm4yvoJUnqzd$sZ>N!8}3%wDR>RkEh0nj&en&%6*w|Epr2&UUOJuW#U9{Cqnbi<+?#( zsK{R4@T|n;eF3Zv{7ple!+XUri|?!$Iu^9@9l~>Q(A;=%i%jt4@(q<_X^%0DxvQFE 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 diff --git a/docs/architecture/assets/IBC_relayer_threads.jpeg b/docs/architecture/assets/IBC_relayer_threads.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..543a29e5d64186d55ba6a8b03eaa7e1363e3de4e GIT binary patch literal 93841 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 z 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 literal 0 HcmV?d00001 diff --git a/relayer/relay/src/config.rs b/relayer/relay/src/config.rs index 2f513bcf9e..9ac0cee1a4 100644 --- a/relayer/relay/src/config.rs +++ b/relayer/relay/src/config.rs @@ -74,6 +74,7 @@ pub struct ChainConfig { pub rpc_addr: net::Address, pub account_prefix: String, pub key_name: String, + pub store_prefix: String, pub client_ids: Vec, #[serde(default = "default::gas")] pub gas: u64, @@ -110,8 +111,10 @@ impl Default for Direction { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct RelayPath { - pub src_port: Option, // default from any source port - pub dest_port: Option, // default from any dest port + pub src_port: Option, // default from any source port + pub dest_port: Option, // default from any dest port + pub src_channel: Option, // default from any source port + pub dest_channel: Option, // default from any dest port #[serde(default)] pub direction: Direction, // default bidirectional } @@ -142,6 +145,7 @@ mod tests { ); let config = parse(path); + println!("{:?}", config); assert!(config.is_ok()); } } diff --git a/relayer/relay/tests/config/fixtures/relayer_conf_example.toml b/relayer/relay/tests/config/fixtures/relayer_conf_example.toml index f8a1bdb33f..9dc41defd1 100644 --- a/relayer/relay/tests/config/fixtures/relayer_conf_example.toml +++ b/relayer/relay/tests/config/fixtures/relayer_conf_example.toml @@ -1,5 +1,3 @@ -# This is an IBC relayer sample configuration - title = "IBC Relayer Config Example" [global] @@ -11,6 +9,7 @@ strategy = "naive" rpc_addr = "localhost:26657" account_prefix = "cosmos" key_name = "testkey" + store_prefix = "ibc" client_ids = ["clA1", "clA2"] gas = 200000 gas_adjustement = 1.3 @@ -19,10 +18,10 @@ strategy = "naive" [[chains]] id = "chain_B" - # rpc_addr = "localhost:26557" - rpc_addr = "localhost:26657" + rpc_addr = "localhost:26557" account_prefix = "cosmos" key_name = "testkey" + store_prefix = "ibc" client_ids = ["clB1"] gas = 200000 gas_adjustement = 1.3 @@ -32,19 +31,25 @@ strategy = "naive" [[connections]] [connections.src] - client_id = "clA1" - connection_id = "conn1-idA-clA1" + client_id = "clB1" + connection_id = "connAtoB" [connections.dest] - client_id = "clB1" - connection_id = "conn1-idB-clB1" + client_id = "clA1" + connection_id = "connBtoA" [[connections.paths]] - src_port = "app1-port-A" - dest_port = "app1-port-B" + src_port = "portA1" + dest_port = "portB1" direction = "unidirectional" + [[connections.paths]] - src_port = "app2-port-A" - dest_port = "app2-port-B" + src_port = "portA2" + dest_port = "portB2" direction = "bidirectional" +[[connections.paths]] + src_port = "portA3" + dest_port = "portB3" + src_channel = "chan3onA" + dest_channel = "chan3onB" From 910d4c9e32289a9754ba398d70d7368ff2048060 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 20 May 2020 10:23:12 +0200 Subject: [PATCH 54/63] Added guards for RelayNextEnv action; termination checks for good env.; state space explosion for malicious env. --- .../ConnectionHandshakeModule.tla | 3 +- .../spec/connection-handshake/Environment.tla | 83 ++++++++++--------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 6e5357e60f..1e1ffb3737 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -337,7 +337,6 @@ Next == \/ /\ inBuf = <<>> /\ ~ CanAdvance /\ UNCHANGED<> - \/ UNCHANGED<> TypeInvariant == @@ -349,6 +348,6 @@ TypeInvariant == ============================================================================= \* Modification History -\* Last modified Tue May 19 17:48:10 CEST 2020 by adi +\* Last modified Wed May 20 08:49:23 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 9941d0c596..13ce964888 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -30,14 +30,13 @@ ASSUME MaxBufLen >= 1 VARIABLES - turn, inBufChainA, \* A buffer (sequence) for messages inbound to chain A. inBufChainB, \* A buffer for messages inbound to chain B. outBufChainA, \* A buffer for messages outgoing from chain A. outBufChainB, \* A buffer for messages outgoing from chain B. storeChainA, \* The local store of chain A. storeChainB, \* The local store of chain B. - maliciousEnv \* If TRUE, environment interferes w/ CH protocol. + maliciousEnv \* If TRUE, environment interferes w/ CH protocol. (************* ChainAConnectionEnds & ChainBConnectionEnds ***************** @@ -84,8 +83,7 @@ chainStoreVars == <> allVars == <> + maliciousEnv>> INSTANCE ICS3Types @@ -208,14 +206,16 @@ RelayNextEnv == THEN msg.connProof.height ELSE storeChainA.latestHeight IN /\ Relay(outBufChainA, inBufChainB) - /\ chmB!UpdateClient(targetHeight) + /\ \/ chmB!CanAdvance /\ chmB!UpdateClient(targetHeight) + \/ ~ chmB!CanAdvance /\ UNCHANGED storeChainB /\ UNCHANGED<> \/ LET msg == Head(outBufChainB) targetHeight == IF MessageTypeIncludesConnProof(msg.type) THEN msg.connProof.height ELSE storeChainB.latestHeight IN /\ Relay(outBufChainB, inBufChainA) - /\ chmA!UpdateClient(targetHeight) + /\ \/ chmA!CanAdvance /\ chmA!UpdateClient(targetHeight) + \/ ~ chmA!CanAdvance /\ UNCHANGED storeChainA /\ UNCHANGED<> @@ -267,19 +267,19 @@ NextEnv == /\ UNCHANGED maliciousEnv \/ /\ RelayNextEnv /\ UNCHANGED maliciousEnv - \/ /\ UNCHANGED <> - \/ /\ ~ maliciousEnv (* Enable malicious env. *) - /\ storeChainA.connection.state # "UNINIT" - /\ storeChainB.connection.state # "UNINIT" - /\ maliciousEnv' = TRUE - /\ MaliciousNextEnv - /\ UNCHANGED<> - \/ /\ maliciousEnv (* A malicious step. *) - /\ MaliciousNextEnv - /\ UNCHANGED<> - \/ /\ maliciousEnv (* Disable malicious env. *) - /\ maliciousEnv' = FALSE - /\ UNCHANGED<> +\* \/ /\ UNCHANGED <> +\* \/ /\ ~ maliciousEnv (* Enable malicious env. *) +\* /\ storeChainA.connection.state # "UNINIT" +\* /\ storeChainB.connection.state # "UNINIT" +\* /\ maliciousEnv' = TRUE +\* /\ MaliciousNextEnv +\* /\ UNCHANGED<> +\* \/ /\ maliciousEnv (* A malicious step. *) +\* /\ MaliciousNextEnv +\* /\ UNCHANGED<> +\* \/ /\ maliciousEnv (* Disable malicious env. *) +\* /\ maliciousEnv' = FALSE +\* /\ UNCHANGED<> (* Enables when the connection is open on both chains. @@ -292,19 +292,12 @@ ICS3ReachTermination == /\ UNCHANGED allVars -(* Enables when both chains attain maximum height, if the connection is still - not opened. - State predicate signaling that the chains cannot progress any further, - and therefore the protocol terminates without successfully opening the - connection. - *) ICS3NonTermination == - /\ (~ chmA!CanAdvance \/ storeChainA.connection.state # "OPEN") - /\ (~ chmB!CanAdvance \/ storeChainB.connection.state # "OPEN") + /\ \/ (~ chmA!CanAdvance /\ storeChainA.connection.state # "OPEN") + \/ (~ chmB!CanAdvance /\ storeChainB.connection.state # "OPEN") /\ UNCHANGED allVars - (****************************************************************************** Main spec. The system comprises the environment plus the two instances of @@ -315,7 +308,6 @@ ICS3NonTermination == (* Initializes both chains, attributing to each a chainID and a client. *) Init == - /\ turn = "ENV" /\ \E clientA \in InitClients({ x.clientID : x \in ChainAConnectionEnds }) : chmA!Init("chainA", clientA, NullConnection) /\ \E clientB \in InitClients({ x.clientID : x \in ChainBConnectionEnds }) : @@ -325,19 +317,19 @@ Init == (* The two ICS3 modules and the environment alternate their steps. *) Next == -\* \/ ICS3ReachTermination -\* \/ ICS3NonTermination - \/ turn = "ENV" /\ NextEnv /\ turn' = "A" - \/ turn = "A" /\ chmA!Next /\ UNCHANGED <> - /\ turn' = "B" - \/ turn = "B" /\ chmB!Next /\ UNCHANGED <> - /\ turn' = "ENV" + \/ ICS3ReachTermination + \/ ICS3NonTermination + \/ NextEnv + \/ chmA!Next /\ UNCHANGED <> + \/ chmB!Next /\ UNCHANGED <> FairProgress == /\ WF_chainAVars(chmA!Next) /\ WF_chainBVars(chmB!Next) /\ WF_<>(RelayNextEnv) +\* /\ \E m \in ConnectionHandshakeMessages : m = Head(inBufChainA) +\* => WF_chainAVars(chmA!ProcessMsg(m)) \* /\ <> ~ maliciousEnv \* /\ <> ~ activeEnv \* /\ \A height \in 1..MaxHeight : WF_storeChainA(chmA!UpdateClient(height)) @@ -360,13 +352,24 @@ TypeInvariant == (* Action ProcessMsg will eventually enable & chainAVars will not be unchanged. *) MessageLivenessPre == - \E m \in ConnectionHandshakeMessages : m = Head(inBufChainA) - => <><>_chainAVars + TRUE +\* /\ \E m \in ConnectionHandshakeMessages : m = Head(inBufChainA) +\* => <><>_chainAVars +\* /\ \E m \in ConnectionHandshakeMessages : m = Head(inBufChainB) +\* => <><>_chainBVars (* Liveness property. - If both chains can progress, we should reach open on both chains. + We expect to always reach an OPEN connection on both chains if the following + conditions hold: + + 1. eventually, the environment stops being malicious, and + + 2, 3. both chains can advance with at least 4 more steps (4 is the minimum + number of steps that are necessary for the chains to reach OPEN) + + 4. *) Termination == []((/\ <> ~ maliciousEnv @@ -394,6 +397,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Tue May 19 18:20:06 CEST 2020 by adi +\* Last modified Wed May 20 10:20:16 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 9ccfc892a74141135d592a71c44f7ecae32a1ec3 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 20 May 2020 19:16:06 +0200 Subject: [PATCH 55/63] After Anca & Zarko's PR suggestions. --- .../ConnectionHandshakeModule.tla | 176 ++++++++++-------- .../spec/connection-handshake/Environment.tla | 53 +++--- .../spec/connection-handshake/ICS3Types.tla | 146 ++++++++++----- 3 files changed, 239 insertions(+), 136 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 1e1ffb3737..7908dfab58 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -12,16 +12,21 @@ This module deals with a high-level spec of the ICS3 protocol, so it is a simplification with respect to ICS3 proper in several regards: + - the modules assumes to run on a chain which we model as a simple advancing height, plus a few more critical fields (see the 'store'), but without any state (e.g., blockchain, transactions, consensus core); + - we model a single connection; establishing multiple connections is not possible; - - we do not perform any cryptographic proofs or proof verifications. + + - we do not perform any cryptographic proof verifications; + - the abstractions we use are higher-level, and slightly different from the ones in ICS3 (see e.g., ConnectionEnd and Connection records). + - the client colocated with the module is simplified, comprising only - a set of heights. + a set of heights (not the actual blockchain headers). ***************************************************************************) @@ -40,12 +45,13 @@ ASSUME Cardinality(ClientIDs) >= 1 VARIABLES (******************************* Store ***************************** + The store record of a chain contains the following fields: - - - id -- a string + + - chainID -- a string Stores the identifier of the chain where this module executes. - - latestHeight -- a Nat + - latestHeight -- a natural number in the range 1..MaxHeight Describes the current height of the chain. - connection -- a connection record. @@ -55,10 +61,23 @@ VARIABLES - client -- a client record. Specifies the state of the client running on this chain. - For a full description of a client and the initialization values - for this record, see the InitClients set below. - - ***************************************************************************) + + A client record contains the following fields: + + - consensusHeights -- a set of heights + Stores the set of all heights (i.e., consensus states) that this + client observed. + + - clientID -- a string + The identifier of the client. + + - latestHeight -- a natural number in the range 1..MaxHeight + Stores the latest height among all the heights in consensusHeights. + + For more details on how clients are initialized, see the operator + ICS3Types.InitClients. + + ***************************************************************************) store, (* A buffer (Sequence) holding any message(s) incoming to this module. *) inBuf, @@ -66,7 +85,8 @@ VARIABLES outBuf -moduleVars == <> +moduleVars == + <> (*************************************************************************** @@ -74,10 +94,19 @@ moduleVars == <> ***************************************************************************) -(* Returns true if 'para' matches the parameters in the local connection, +(* Simple computation returning the maximum out of two numbers 'a' and 'b'. + *) +MAX(a, b) == + IF a > b THEN a ELSE b + + +(* Validates a connection parameter. + + Returns true if 'para' matches the parameters in the local connection, and returns false otherwise. + *) -CheckLocalParameters(para) == +ValidConnectionParameters(para) == LET local == store.connection.parameters.localEnd remote == store.connection.parameters.remoteEnd IN /\ local.connectionID = para.localEnd.connectionID @@ -86,21 +115,16 @@ CheckLocalParameters(para) == /\ remote.clientID = para.remoteEnd.clientID -(* Validates a ConnectionParameter. +(* Validates a connection parameter local end. + Expects as input a ConnectionParameter 'para' and returns true or false. - - This is a basic validation step, making sure that 'para' is valid with - respect to module-level constants ConnectionIDs and ClientIDs. - If there is a connection in the store, this also validates 'para' - against the parameters of an existing connection by relying on the - state predicate CheckLocalParameters. + This is a basic validation step, making sure that the local end in 'para' + is valid with respect to module-level constants ConnectionIDs and ClientIDs. + *) -ValidConnectionParameters(para) == +ValidLocalEnd(para) == /\ para.localEnd.connectionID \in ConnectionIDs /\ para.localEnd.clientID \in ClientIDs - /\ \/ store.connection = NullConnection - \/ /\ store.connection /= NullConnection - /\ CheckLocalParameters(para) (* Operator for reversing the connection ends. @@ -114,33 +138,34 @@ FlipConnectionParameters(para) == remoteEnd |-> para.localEnd] -(* Operator for construcing a connection proof. +(* Operator for constructing a connection proof. The connection proof is used to demonstrate to another chain that the local store on this chain comprises a connection in a certain state. *) -GetConnProof(connState) == - [height |-> store.latestHeight, - connectionState |-> connState] +GetConnProof(myConnection) == + [connection |-> myConnection] -(* Operator for construcing a client proof. +(* Operator for constructing a client proof. *) GetClientProof == - [height |-> store.client.latestHeight] + [latestHeight |-> store.client.latestHeight, + consensusHeights |-> store.client.consensusHeights] (* Verification of a connection proof. - This is a state predicate returning true if the following holds: the local - client on this chain stores the height reported in the proof. Note that this - condition should eventually become true, assuming a correct environment, which - should periodically update the client on each chain; see actions 'GoodNextEnv' - and 'UpdateClient'. + This is a state predicate returning true if the following holds: + - the state of connection in this proof should match with input parameter + 'expectedState'; and + - the connection parameters in this proof should match with the flipped version + of the input 'expectedParams'. *) -VerifyConnProof(cp) == - /\ cp.height \in store.client.consensusStates +VerifyConnProof(cp, expectedState, expectedParams) == + /\ cp.connection.state = expectedState + /\ cp.connection.parameters = FlipConnectionParameters(expectedParams) (* Verification of a client proof. @@ -150,7 +175,8 @@ VerifyConnProof(cp) == this chain. *) VerifyClientProof(cp) == - cp.height <= store.latestHeight + /\ cp.latestHeight <= store.latestHeight (* Consistency height check. *) + /\ cp.latestHeight \in cp.consensusHeights (* Client verification step. *) (*************************************************************************** @@ -169,7 +195,7 @@ NewStore(newCon) == (* Handles a "ICS3MsgInit" message 'm'. - + Primes the store.connection to become initialized with the parameters specified in 'm'. Also creates a reply message, enqueued on the outgoing buffer. @@ -177,13 +203,14 @@ NewStore(newCon) == HandleInitMsg(m) == LET newCon == [parameters |-> m.parameters, state |-> "INIT"] - sProof == GetConnProof("INIT") - cProof == GetClientProof + myConnProof == GetConnProof(newCon) + myClientProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "ICS3MsgTry", - connProof |-> sProof, - clientProof |-> cProof] IN - IF /\ ValidConnectionParameters(m.parameters) + proofHeight |-> store.latestHeight, + connProof |-> myConnProof, + clientProof |-> myClientProof] IN + IF /\ ValidLocalEnd(m.parameters) (* Basic validation of localEnd in parameters. *) /\ store.connection.state = "UNINIT" THEN [out |-> Append(outBuf, replyMsg), store |-> NewStore(newCon)] @@ -194,15 +221,15 @@ HandleInitMsg(m) == (* State predicate, guarding the handler for the Try msg. If any of these preconditions does not hold, the message - is dropped. + is dropped. *) PreconditionsTryMsg(m) == - /\ ValidConnectionParameters(m.parameters) - /\ \/ store.connection.state = "UNINIT" + /\ \/ /\ store.connection.state = "UNINIT" + /\ ValidLocalEnd(m.parameters) \/ /\ store.connection.state = "INIT" - /\ CheckLocalParameters(m.parameters) - /\ m.connProof.connectionState = "INIT" - /\ VerifyConnProof(m.connProof) + /\ ValidConnectionParameters(m.parameters) + /\ m.proofHeight \in store.client.consensusHeights (* Consistency height check. *) + /\ VerifyConnProof(m.connProof, "INIT", m.parameters) /\ VerifyClientProof(m.clientProof) @@ -211,12 +238,13 @@ PreconditionsTryMsg(m) == HandleTryMsg(m) == LET newCon == [parameters |-> m.parameters, state |-> "TRYOPEN"] - sProof == GetConnProof("TRYOPEN") - cProof == GetClientProof + myConnProof == GetConnProof(newCon) + myClientProof == GetClientProof replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), type |-> "ICS3MsgAck", - connProof |-> sProof, - clientProof |-> cProof] IN + proofHeight |-> store.latestHeight, + connProof |-> myConnProof, + clientProof |-> myClientProof] IN IF PreconditionsTryMsg(m) THEN [out |-> Append(outBuf, replyMsg), store |-> NewStore(newCon)] @@ -229,9 +257,10 @@ HandleTryMsg(m) == PreconditionsAckMsg(m) == /\ \/ store.connection.state = "INIT" \/ store.connection.state = "TRYOPEN" - /\ CheckLocalParameters(m.parameters) - /\ m.connProof.connectionState = "TRYOPEN" - /\ VerifyConnProof(m.connProof) + /\ ValidConnectionParameters(m.parameters) + /\ m.proofHeight \in store.client.consensusHeights (* Consistency height check. *) + /\ VerifyConnProof(m.connProof, "TRYOPEN", m.parameters) + /\ VerifyClientProof(m.clientProof) (* Handles a "ICS3MsgAck" message. @@ -239,11 +268,11 @@ PreconditionsAckMsg(m) == HandleAckMsg(m) == LET newCon == [parameters |-> m.parameters, state |-> "OPEN"] - sProof == GetConnProof("OPEN") - cProof == GetClientProof + myConnProof == GetConnProof(newCon) replyMsg == [parameters |-> FlipConnectionParameters(m.parameters), + proofHeight |-> store.latestHeight, type |-> "ICS3MsgConfirm", - connProof |-> sProof] IN + connProof |-> myConnProof] IN IF PreconditionsAckMsg(m) THEN [out |-> Append(outBuf, replyMsg), store |-> NewStore(newCon)] @@ -255,9 +284,9 @@ HandleAckMsg(m) == *) PreconditionsConfirmMsg(m) == /\ store.connection.state = "TRYOPEN" - /\ CheckLocalParameters(m.parameters) - /\ m.connProof.connectionState = "OPEN" - /\ VerifyConnProof(m.connProof) + /\ ValidConnectionParameters(m.parameters) + /\ m.proofHeight \in store.client.consensusHeights (* Consistency height check. *) + /\ VerifyConnProof(m.connProof, "OPEN", m.parameters) (* Handles a "ICS3MsgConfirm" message. @@ -293,11 +322,13 @@ CanAdvance == This will also advance the chain height. *) UpdateClient(height) == - \/ /\ height \notin store.client.consensusStates + \/ /\ height \notin store.client.consensusHeights + (* Warning: following line should provoke a deadlock in ICS3 protocol. *) + /\ height >= store.client.latestHeight /\ store' = [store EXCEPT !.latestHeight = @ + 1, - !.client.consensusStates = @ \cup {height}, - !.client.latestHeight = height] - \/ /\ height \in store.client.consensusStates + !.client.consensusHeights = @ \cup {height}, + !.client.latestHeight = MAX(height, store.client.latestHeight)] + \/ /\ height \in store.client.consensusHeights /\ UNCHANGED store @@ -322,10 +353,10 @@ ProcessMsg(m) == ***************************************************************************) -Init(chainID, client, connection) == - /\ store = [id |-> chainID, +Init(chainID, client) == + /\ store = [chainID |-> chainID, latestHeight |-> 1, - connection |-> connection, + connection |-> NullConnection, client |-> client] @@ -341,13 +372,12 @@ Next == TypeInvariant == /\ inBuf \in Seq(ConnectionHandshakeMessages) \union {<<>>} - /\ outBuf \in Seq(ConnectionHandshakeMessages) \union {<<>>} - /\ store.connection \in Connections - /\ store.client.clientID \in ClientIDs \union {NullClientID} + /\ outBuf \in Seq(ConnectionHandshakeMessages) \union {<<>>} + /\ store \in Stores ============================================================================= \* Modification History -\* Last modified Wed May 20 08:49:23 CEST 2020 by adi +\* Last modified Wed May 20 16:27:30 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 13ce964888..5d93980452 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -66,6 +66,8 @@ AllClientIDs == AllConnectionIDs == { x.connectionID : x \in AllConnectionEnds } +AllChainIDs == + { "chainA", "chainB" } (* Bundle with variables that chain A has access to. *) chainAVars == < "clientIDChainB"] remoteEnd |-> [connectionID |-> "connBtoA", clientID |-> "clientIDChainA"] - + 2. Environment injects, maliciously, a ICS3MsgInit to chain B with the following parameters: @@ -137,7 +139,7 @@ chmB == INSTANCE ConnectionHandshakeModule clientID |-> "clientIDChainA"] remoteEnd |-> [connectionID |-> "connBtoA", clientID |-> "clientIDChainA"] - + Notice that the localEnd is correct, so chain B will validate and process this message; the remoteEnd is incorrect, however, but chain B is not able to validate that part of the connection, so it will @@ -147,35 +149,39 @@ chmB == INSTANCE ConnectionHandshakeModule updates its store.connection with the parameters from step 1 above. At this point, chain A "locks onto" these parameters and will not accept any others. Chain A also produces a ICS3MsgTry message. - + 3. Chain B processes the ICS3MsgInit (action HandleInitMsg) and updates its store.connection with the parameters from step 2 above. Chain B "locks onto" these parameters and will not accept any others. At this step, chain B produces a ICS3MsgTry message with the local parameters from its connection. - + Both chains will be locked on a different set of connection parameters, and neither chain will accept their corresponding ICS3MsgTry, hence a deadlock. To avoid this problem, we prevent the environment from acting maliciously in the preliminary parts of the ICS3 protocol, until both chains finish locking on the same set of connection parameters. + *) InitEnv == /\ maliciousEnv = FALSE /\ \/ /\ inBufChainA \in {<> : (* ICS3MsgInit to chain A. *) - msg \in InitMsgs(ChainAConnectionEnds, ChainBConnectionEnds)} + msg \in InitMsgs(ChainAConnectionEnds, ChainBConnectionEnds)} /\ inBufChainB = <<>> \/ /\ inBufChainB \in {<> : (* ICS3MsgInit to chain B. *) - msg \in InitMsgs(ChainBConnectionEnds, ChainAConnectionEnds)} - /\ inBufChainB = <<>> + msg \in InitMsgs(ChainBConnectionEnds, ChainAConnectionEnds)} + /\ inBufChainA = <<>> \/ /\ inBufChainA \in {<> : (* ICS3MsgInit to both chains. *) - msg \in InitMsgs(ChainAConnectionEnds, ChainBConnectionEnds)} + msg \in InitMsgs(ChainAConnectionEnds, ChainBConnectionEnds)} /\ inBufChainB \in {<> : - msg \in InitMsgs(ChainBConnectionEnds, ChainAConnectionEnds)} + msg \in InitMsgs(ChainBConnectionEnds, ChainAConnectionEnds)} /\ outBufChainA = <<>> /\ outBufChainB = <<>> +(* + TODO EXPLAIN + *) Relay(from, to) == /\ from # <<>> /\ Len(to) < MaxBufLen - 1 @@ -203,17 +209,21 @@ GoodNextEnv == RelayNextEnv == \/ LET msg == Head(outBufChainA) targetHeight == IF MessageTypeIncludesConnProof(msg.type) - THEN msg.connProof.height + THEN msg.proofHeight ELSE storeChainA.latestHeight IN /\ Relay(outBufChainA, inBufChainB) + (* TODO: remove following line to fix the deadlock. *) +\* /\ chmB!UpdateClient(storeChainA.latestHeight) /\ \/ chmB!CanAdvance /\ chmB!UpdateClient(targetHeight) \/ ~ chmB!CanAdvance /\ UNCHANGED storeChainB /\ UNCHANGED<> \/ LET msg == Head(outBufChainB) targetHeight == IF MessageTypeIncludesConnProof(msg.type) - THEN msg.connProof.height + THEN msg.proofHeight ELSE storeChainB.latestHeight IN /\ Relay(outBufChainB, inBufChainA) + (* TODO: remove following line to fix the deadlock. *) +\* /\ chmA!UpdateClient(storeChainB.latestHeight) /\ \/ chmA!CanAdvance /\ chmA!UpdateClient(targetHeight) \/ ~ chmA!CanAdvance /\ UNCHANGED storeChainA /\ UNCHANGED<> @@ -292,12 +302,12 @@ ICS3ReachTermination == /\ UNCHANGED allVars - ICS3NonTermination == /\ \/ (~ chmA!CanAdvance /\ storeChainA.connection.state # "OPEN") \/ (~ chmB!CanAdvance /\ storeChainB.connection.state # "OPEN") /\ UNCHANGED allVars + (****************************************************************************** Main spec. The system comprises the environment plus the two instances of @@ -309,9 +319,9 @@ ICS3NonTermination == (* Initializes both chains, attributing to each a chainID and a client. *) Init == /\ \E clientA \in InitClients({ x.clientID : x \in ChainAConnectionEnds }) : - chmA!Init("chainA", clientA, NullConnection) + chmA!Init("chainA", clientA) /\ \E clientB \in InitClients({ x.clientID : x \in ChainBConnectionEnds }) : - chmB!Init("chainB", clientB, NullConnection) + chmB!Init("chainB", clientB) /\ InitEnv @@ -367,12 +377,12 @@ MessageLivenessPre == 1. eventually, the environment stops being malicious, and 2, 3. both chains can advance with at least 4 more steps (4 is the minimum - number of steps that are necessary for the chains to reach OPEN) - - 4. + number of steps that are necessary for the chains to reach OPEN). *) Termination == []((/\ <> ~ maliciousEnv + (* TODO: add activeEnv mechanism, to disable GoodNextEnv sub-action. *) +\* /\ <> ~ activeEnv /\ storeChainA.latestHeight < MaxHeight - 4 /\ storeChainB.latestHeight < MaxHeight - 4 /\ MessageLivenessPre) @@ -395,8 +405,9 @@ ConsistencyProperty == Consistency == [] ConsistencyProperty + ============================================================================= \* Modification History -\* Last modified Wed May 20 10:20:16 CEST 2020 by adi +\* Last modified Wed May 20 16:45:24 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/ICS3Types.tla b/verification/spec/connection-handshake/ICS3Types.tla index 5aed28e75c..4b633353c0 100644 --- a/verification/spec/connection-handshake/ICS3Types.tla +++ b/verification/spec/connection-handshake/ICS3Types.tla @@ -8,23 +8,24 @@ This module includes common domain definitions that other modules will extend. - ***************************************************************************) EXTENDS Naturals CONSTANTS MaxHeight, AllConnectionIDs, - AllClientIDs + AllClientIDs, + AllChainIDs + +(******************************* InitClients ******************************** -(******************************* InitClients ***************************** A set of records describing the possible initial values for the clients on a chain. A client record contains the following fields: - - consensusStates -- a set of heights, each height being a Nat + - consensusHeights -- a set of heights Stores the set of all heights (i.e., consensus states) that this client observed. At initialization time, the client only observes the first height, so the only possible value for this record is @@ -34,18 +35,18 @@ CONSTANTS MaxHeight, The identifier of the client. This is expected as a parameter, since it is a chain-specific field at initialization time. - - latestHeight -- a natural number - Stores the latest height among all the heights in consensusStates. + - latestHeight -- a number representing a (consensus) height + Stores the latest height among all the heights in consensusHeights. Initialized to 1. ***************************************************************************) InitClients(specificClientIDs) == [ - consensusStates : {{1}}, + consensusHeights : {{1}}, clientID : specificClientIDs, latestHeight : {1} ] - + (***************************** InitMsgs *********************************** @@ -110,9 +111,10 @@ NullClientID == NullConnectionID == "NULLConnectionID" + (******************************* NullConnectionEnd ************************* - A special record defining an uninitialized connection end. + A special record defining an uninitialized connection end record. ***************************************************************************) NullConnectionEnd == @@ -124,7 +126,7 @@ NullConnectionEnd == (******************************* NullConnectionParameters ****************** - A record defining the special null connection parameters. + A record defining the special null connection parameters record. ***************************************************************************) NullConnectionParameters == @@ -135,8 +137,9 @@ NullConnectionParameters == (******************************* ConnectionEnds ***************************** - A set of connection end records. - A connection end record contains the following fields: + + A set of connection end records. + A connection end record contains the following fields: - connectionID -- a string Stores the identifier of this connection, specific to a chain. @@ -153,15 +156,16 @@ ConnectionEnds == (******************************* ConnectionParameters ********************** - A set of connection parameter records. - A connection parameter record contains the following fields: + + A set of connection parameter records. + A connection parameter record contains the following fields: - localEnd -- a connection end Specifies the local connection details (i.e., connection ID and client ID). - remoteEnd -- a connection end - Specifies the local connection details. + Specifies the remote connection details. ***************************************************************************) ConnectionParameters == @@ -188,7 +192,8 @@ NullConnection == [ (******************************* Connections ******************************* - A set of connection records. + + The set of possible connection records. A connection record contains the following fields: - parameters -- a connection parameters record @@ -205,52 +210,54 @@ Connections == (******************************* ConnProof ********************************* + A set of records describing the possible values for connection proofs. - - A connection proof record contains the following fields: - - connectionState -- a string - Captures the state of the connection in the local store of the module - which created this proof. + A connection proof record contains a single field: - - height -- a Nat - The current height (latestHeight) of the chain at the moment when the - module created this proof. + - connection -- a connection record + This is the connection (in the local store of a chain) at the moment + when the module created this proof. ***************************************************************************) ConnProofs == [ - connectionState : ICS3ConnectionStates, - height : 1..MaxHeight + connection : Connections ] +(******************************* Heights *********************************** + + The set of all possible heights that a chain can assume throughout any + execution. + + ***************************************************************************) +Heights == + 1..MaxHeight + + (******************************* ClientProofs ******************************* + A set of records describing the possible values for client proofs. - A client proof record contains the following fields: + A client proof record contains two fields: + + - latestHeight -- a number representing a height + The current height (latestHeight) of the client (in the local store of a + chain) at the moment when the ICS3 module created this proof. - - height -- a Nat - The current height (latestHeight) of the client colocated with module - which created this proof. + - consensusHeights -- a set of heights + The set of heights of the client colocated with module which created + this proof. ***************************************************************************) ClientProofs == [ - height : 1..MaxHeight + latestHeight : Heights, + consensusHeights : SUBSET Heights ] -(******************************* Heights *********************************** - - The set of all possible heights that a chain can assume throughout any - execution. - - ***************************************************************************) -Heights == - 1..MaxHeight - - (*********************** ConnectionHandshakeMessages *********************** The set of ConnectionHandshakeMessage records. @@ -267,6 +274,7 @@ ConnectionHandshakeMessages == [ type : {"ICS3MsgTry"}, parameters : ConnectionParameters, + proofHeight : Heights, connProof : ConnProofs, clientProof : ClientProofs ] @@ -274,6 +282,7 @@ ConnectionHandshakeMessages == [ type : {"ICS3MsgAck"}, parameters : ConnectionParameters, + proofHeight : Heights, connProof : ConnProofs, clientProof : ClientProofs ] @@ -281,16 +290,69 @@ ConnectionHandshakeMessages == [ type : {"ICS3MsgConfirm"}, parameters : ConnectionParameters, + proofHeight : Heights, connProof : ConnProofs ] + +(********************** MessageTypeIncludesConnProof *********************** + + Operator that evaluates to true if the message type (input parameter + 'type') refers to a message that includes a connection proof. + + ***************************************************************************) MessageTypeIncludesConnProof(type) == type \in {"ICS3MsgTry", "ICS3MsgAck", "ICS3MsgConfirm"} +(******************************* Clients *********************************** + + A set of records describing all the possible values for the + clients on a chain. + + See client record description above (within the InitClients operator). + + ***************************************************************************) +Clients == + [ + consensusHeights : SUBSET Heights, + clientID : AllClientIDs \union { NullClientID }, + latestHeight : Heights + ] + +(******************************* Stores ************************************* + + The set of store records. + A store record represents the local storage of a chain. This record + contains the following fields: + + - chainID -- a string + Stores the identifier of the chain where this module executes. + + - latestHeight -- a number representing a height + Describes the current height of the chain. + + - connection -- a connection record + Captures all the details of the connection on this chain. + For a full description of a connection record, see the + 'Environment.Connections' set. + + - client -- a client record. + Specifies the state of the client running on this chain. + + ***************************************************************************) +Stores == + [ + chainID : AllChainIDs, + latestHeight : Heights, + connection : Connections \union { NullConnection }, + client : Clients + ] + + ============================================================================= \* Modification History -\* Last modified Tue May 19 17:44:07 CEST 2020 by adi +\* Last modified Wed May 20 16:53:21 CEST 2020 by adi \* Created Mon May 18 17:53:08 CEST 2020 by adi From 90b125213e372bfadebfd17890258cbdd916e67a Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Wed, 20 May 2020 19:44:30 +0200 Subject: [PATCH 56/63] Cleaned-up; removed malicious env. --- .../spec/connection-handshake/Environment.tla | 232 ++++++------------ 1 file changed, 74 insertions(+), 158 deletions(-) diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 5d93980452..dcaa57cce5 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -35,8 +35,7 @@ VARIABLES outBufChainA, \* A buffer for messages outgoing from chain A. outBufChainB, \* A buffer for messages outgoing from chain B. storeChainA, \* The local store of chain A. - storeChainB, \* The local store of chain B. - maliciousEnv \* If TRUE, environment interferes w/ CH protocol. + storeChainB \* The local store of chain B. (************* ChainAConnectionEnds & ChainBConnectionEnds ***************** @@ -69,6 +68,19 @@ AllConnectionIDs == AllChainIDs == { "chainA", "chainB" } +ChainAClientIDs == + { x.clientID : x \in ChainAConnectionEnds } + +ChainBClientIDs == + { x.clientID : x \in ChainBConnectionEnds } + +ChainAConnectionIDs == + { x.connectionID : x \in ChainAConnectionEnds } + +ChainBConnectionIDs == + { x.connectionID : x \in ChainBConnectionEnds } + + (* Bundle with variables that chain A has access to. *) chainAVars == <> (* Local chain store. *) -(* All variables specific to chains. *) +(* All variables specific to both chains. *) chainStoreVars == <> allVars == <> + outBufChainA, outBufChainB>> +(* Separate module with the common type definitions. *) INSTANCE ICS3Types chmA == INSTANCE ConnectionHandshakeModule @@ -95,8 +107,8 @@ chmA == INSTANCE ConnectionHandshakeModule inBuf <- inBufChainA, outBuf <- outBufChainA, store <- storeChainA, - ConnectionIDs <- { x.connectionID : x \in ChainAConnectionEnds }, - ClientIDs <- { x.clientID : x \in ChainAConnectionEnds } + ConnectionIDs <- ChainAConnectionIDs, + ClientIDs <- ChainAClientIDs chmB == INSTANCE ConnectionHandshakeModule @@ -104,8 +116,8 @@ chmB == INSTANCE ConnectionHandshakeModule inBuf <- inBufChainB, outBuf <- outBufChainB, store <- storeChainB, - ConnectionIDs <- { x.connectionID : x \in ChainBConnectionEnds }, - ClientIDs <- { x.clientID : x \in ChainBConnectionEnds } + ConnectionIDs <- ChainBConnectionIDs, + ClientIDs <- ChainBClientIDs (*************************************************************************** @@ -118,53 +130,8 @@ chmB == INSTANCE ConnectionHandshakeModule This action kick-starts the ICS3 protocol by assigning an ICS3MsgInit msg to either of the two chains (or both). - Initially, the environment is non-malicious. The environment starts - acting maliciously once the connection on both chains transitions out - of state "UNINIT" (the initials state). It is important to - initialize the protocol like this, otherwise the env. can provoke a - deadlock. This can happen with the following sequence of actions: - - 1. Environment injects a ICS3MsgInit to chain A with the following - correct parameters: - - localEnd |-> [connectionID |-> "connAtoB", - clientID |-> "clientIDChainB"] - remoteEnd |-> [connectionID |-> "connBtoA", - clientID |-> "clientIDChainA"] - - 2. Environment injects, maliciously, a ICS3MsgInit to chain B with - the following parameters: - - localEnd |-> [connectionID |-> "connBtoA", - clientID |-> "clientIDChainA"] - remoteEnd |-> [connectionID |-> "connBtoA", - clientID |-> "clientIDChainA"] - - Notice that the localEnd is correct, so chain B will validate and - process this message; the remoteEnd is incorrect, however, but chain - B is not able to validate that part of the connection, so it will - accept it as it is. - - 2. Chain A processes the ICS3MsgInit (action HandleInitMsg) and - updates its store.connection with the parameters from step 1 above. - At this point, chain A "locks onto" these parameters and will not - accept any others. Chain A also produces a ICS3MsgTry message. - - 3. Chain B processes the ICS3MsgInit (action HandleInitMsg) and - updates its store.connection with the parameters from step 2 above. - Chain B "locks onto" these parameters and will not accept any others. - At this step, chain B produces a ICS3MsgTry message with the local - parameters from its connection. - - Both chains will be locked on a different set of connection parameters, - and neither chain will accept their corresponding ICS3MsgTry, hence a - deadlock. To avoid this problem, we prevent the environment from - acting maliciously in the preliminary parts of the ICS3 protocol, until - both chains finish locking on the same set of connection parameters. - *) InitEnv == - /\ maliciousEnv = FALSE /\ \/ /\ inBufChainA \in {<> : (* ICS3MsgInit to chain A. *) msg \in InitMsgs(ChainAConnectionEnds, ChainBConnectionEnds)} /\ inBufChainB = <<>> @@ -175,27 +142,31 @@ InitEnv == msg \in InitMsgs(ChainAConnectionEnds, ChainBConnectionEnds)} /\ inBufChainB \in {<> : msg \in InitMsgs(ChainBConnectionEnds, ChainAConnectionEnds)} - /\ outBufChainA = <<>> + /\ outBufChainA = <<>> (* Output buffers should be empty initially. *) /\ outBufChainB = <<>> -(* - TODO EXPLAIN +(* Relay sub-action of the environment. + + This performs a basic relaying step, that is, passing a message from the + output buffer of one of the chains (paramter 'from') into the input buffer + of another chain (parameter 'to'). + *) -Relay(from, to) == +RelayMessage(from, to) == /\ from # <<>> /\ Len(to) < MaxBufLen - 1 /\ to' = Append(to, Head(from)) /\ from' = Tail(from) -(* Default next (good) action for Environment. - - TODO: should advance the height non-deterministically? +(* Default next action for environment. - May change either of the store of chain A or B. + This action may change (non-deterministically) either of the store of chain A + or B by advancing the height or updating the client on that chain. + *) -GoodNextEnv == +DefaultNextEnv == \/ /\ chmA!CanAdvance /\ \/ chmA!AdvanceChainHeight \/ chmA!UpdateClient(storeChainB.latestHeight) @@ -206,12 +177,19 @@ GoodNextEnv == /\ UNCHANGED<> +(* Relaying action for the environment. + + This action performs a relaying step: moving a message between the output + buffer of a chain to the input buffer of the other chain, and updating accordingly + the client on the latter chain. + + *) RelayNextEnv == \/ LET msg == Head(outBufChainA) targetHeight == IF MessageTypeIncludesConnProof(msg.type) THEN msg.proofHeight ELSE storeChainA.latestHeight - IN /\ Relay(outBufChainA, inBufChainB) + IN /\ RelayMessage(outBufChainA, inBufChainB) (* TODO: remove following line to fix the deadlock. *) \* /\ chmB!UpdateClient(storeChainA.latestHeight) /\ \/ chmB!CanAdvance /\ chmB!UpdateClient(targetHeight) @@ -221,7 +199,7 @@ RelayNextEnv == targetHeight == IF MessageTypeIncludesConnProof(msg.type) THEN msg.proofHeight ELSE storeChainB.latestHeight - IN /\ Relay(outBufChainB, inBufChainA) + IN /\ RelayMessage(outBufChainB, inBufChainA) (* TODO: remove following line to fix the deadlock. *) \* /\ chmA!UpdateClient(storeChainB.latestHeight) /\ \/ chmA!CanAdvance /\ chmA!UpdateClient(targetHeight) @@ -229,80 +207,39 @@ RelayNextEnv == /\ UNCHANGED<> -(* Environment malicious behavior. - - The environment injects a msg. in the buffer of one of the chains. - This interferes with the ICS3 protocol by introducing additional - messages that are usually incorrect. - - Without the first constraint, on the "Len(inBufChainA)" and - "Len(inBufChainB)", Env could fill buffers (DoS attack). This can - lead to a deadlock, because chains will simply be unable to reply - to each other. - *) -MaliciousNextEnv == - \/ /\ Len(inBufChainA) < MaxBufLen - 1 - /\ inBufChainA' \in {Append(inBufChainA, arbitraryMsg) : - arbitraryMsg \in {msg \in ConnectionHandshakeMessages : - msg.type = "ICS3MsgTry"}} - /\ UNCHANGED inBufChainB - \/ /\ Len(inBufChainB) < MaxBufLen - 1 - /\ inBufChainB' \in {Append(inBufChainB, arbitraryMsg) : - arbitraryMsg \in {msg \in ConnectionHandshakeMessages : - msg.type = "ICS3MsgTry"}} -\* arbitraryMsg \in ConnectionHandshakeMessages -\* /\ arbitraryMsg.type = "ICS3MsgTry"} - /\ UNCHANGED inBufChainA - - (* Environment next action. -TODO: explain the relaying functionality and additional variables. + There are two possible actions that the environment may perform: - There are four possible actions that the environment may perform: - - 1. A good step: the environment advances or updates the client on + 1. A default step: the environment advances or updates the client on one of the two chains. - 2. The environment becomes malicious, as a result of both chains - advancing past the UNINIT step (i.e., after both chains finished - locking on to a set of connection parameters). - - 3. A malicious step. + 2. The environment performs a relaying step. - 4. The environment stops acting maliciously. *) NextEnv == - \/ /\ GoodNextEnv (* A good step. *) - /\ UNCHANGED maliciousEnv - \/ /\ RelayNextEnv - /\ UNCHANGED maliciousEnv -\* \/ /\ UNCHANGED <> -\* \/ /\ ~ maliciousEnv (* Enable malicious env. *) -\* /\ storeChainA.connection.state # "UNINIT" -\* /\ storeChainB.connection.state # "UNINIT" -\* /\ maliciousEnv' = TRUE -\* /\ MaliciousNextEnv -\* /\ UNCHANGED<> -\* \/ /\ maliciousEnv (* A malicious step. *) -\* /\ MaliciousNextEnv -\* /\ UNCHANGED<> -\* \/ /\ maliciousEnv (* Disable malicious env. *) -\* /\ maliciousEnv' = FALSE -\* /\ UNCHANGED<> + \/ DefaultNextEnv + \/ RelayNextEnv (* Enables when the connection is open on both chains. State predicate signaling that the protocol terminated correctly. + *) -ICS3ReachTermination == +ICS3ReachedOpenConnection == /\ storeChainA.connection.state = "OPEN" /\ storeChainB.connection.state = "OPEN" /\ UNCHANGED allVars -ICS3NonTermination == +(* Enables when both chains are stuck, i.e., unable to progress while + their connection is not opened. + + State predicate signaling that the protocol terminated unsucessfully. + + *) +ICS3ImpossibleToAdvance == /\ \/ (~ chmA!CanAdvance /\ storeChainA.connection.state # "OPEN") \/ (~ chmB!CanAdvance /\ storeChainB.connection.state # "OPEN") /\ UNCHANGED allVars @@ -318,34 +255,30 @@ ICS3NonTermination == (* Initializes both chains, attributing to each a chainID and a client. *) Init == - /\ \E clientA \in InitClients({ x.clientID : x \in ChainAConnectionEnds }) : + /\ \E clientA \in InitClients(ChainAClientIDs) : chmA!Init("chainA", clientA) - /\ \E clientB \in InitClients({ x.clientID : x \in ChainBConnectionEnds }) : + /\ \E clientB \in InitClients(ChainBClientIDs) : chmB!Init("chainB", clientB) /\ InitEnv -(* The two ICS3 modules and the environment alternate their steps. *) +(* The two ICS3 modules and the environment alternate their steps + non-deterministically. Eventually, the execution ends with either + successful (ICS3ReachedOpenConnection sub-action) or unsuccesfull + (ICS3ImpossibleToAdvance sub-action) termination. +*) Next == - \/ ICS3ReachTermination - \/ ICS3NonTermination + \/ ICS3ReachedOpenConnection + \/ ICS3ImpossibleToAdvance \/ NextEnv - \/ chmA!Next /\ UNCHANGED <> - \/ chmB!Next /\ UNCHANGED <> + \/ chmA!Next /\ UNCHANGED chainBVars + \/ chmB!Next /\ UNCHANGED chainAVars FairProgress == /\ WF_chainAVars(chmA!Next) /\ WF_chainBVars(chmB!Next) /\ WF_<>(RelayNextEnv) -\* /\ \E m \in ConnectionHandshakeMessages : m = Head(inBufChainA) -\* => WF_chainAVars(chmA!ProcessMsg(m)) -\* /\ <> ~ maliciousEnv -\* /\ <> ~ activeEnv -\* /\ \A height \in 1..MaxHeight : WF_storeChainA(chmA!UpdateClient(height)) -\* /\ \A height \in 1..MaxHeight : WF_storeChainB(chmB!UpdateClient(height)) -\* /\ WF_storeChainA(chmA!AdvanceChainHeight) -\* /\ WF_storeChainB(chmB!AdvanceChainHeight) Spec == @@ -359,33 +292,16 @@ TypeInvariant == /\ chmB!TypeInvariant -(* Action ProcessMsg will eventually enable & chainAVars - will not be unchanged. *) -MessageLivenessPre == - TRUE -\* /\ \E m \in ConnectionHandshakeMessages : m = Head(inBufChainA) -\* => <><>_chainAVars -\* /\ \E m \in ConnectionHandshakeMessages : m = Head(inBufChainB) -\* => <><>_chainBVars - - (* Liveness property. - + We expect to always reach an OPEN connection on both chains if the following - conditions hold: - - 1. eventually, the environment stops being malicious, and - - 2, 3. both chains can advance with at least 4 more steps (4 is the minimum + condition holds: + both chains can advance with at least 4 more steps (4 is the minimum number of steps that are necessary for the chains to reach OPEN). *) Termination == - []((/\ <> ~ maliciousEnv - (* TODO: add activeEnv mechanism, to disable GoodNextEnv sub-action. *) -\* /\ <> ~ activeEnv - /\ storeChainA.latestHeight < MaxHeight - 4 - /\ storeChainB.latestHeight < MaxHeight - 4 - /\ MessageLivenessPre) + []((/\ storeChainA.latestHeight < MaxHeight - 4 + /\ storeChainB.latestHeight < MaxHeight - 4) => <> (/\ storeChainA.connection.state = "OPEN" /\ storeChainB.connection.state = "OPEN")) @@ -408,6 +324,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Wed May 20 16:45:24 CEST 2020 by adi +\* Last modified Wed May 20 19:42:17 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From d7988504bd3255bc989da81b902654edfae430c0 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 21 May 2020 08:14:32 +0200 Subject: [PATCH 57/63] Recreating the deadlock #1 --- .../spec/connection-handshake/Environment.tla | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index dcaa57cce5..e355cd3585 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -164,17 +164,17 @@ RelayMessage(from, to) == This action may change (non-deterministically) either of the store of chain A or B by advancing the height or updating the client on that chain. - + *) -DefaultNextEnv == - \/ /\ chmA!CanAdvance - /\ \/ chmA!AdvanceChainHeight - \/ chmA!UpdateClient(storeChainB.latestHeight) - /\ UNCHANGED<> - \/ /\ chmB!CanAdvance - /\ \/ chmB!AdvanceChainHeight - \/ chmB!UpdateClient(storeChainA.latestHeight) - /\ UNCHANGED<> +\*DefaultNextEnv == +\* \/ /\ chmA!CanAdvance +\* /\ \/ chmA!AdvanceChainHeight +\* \/ chmA!UpdateClient(storeChainB.latestHeight) +\* /\ UNCHANGED<> +\* \/ /\ chmB!CanAdvance +\* /\ \/ chmB!AdvanceChainHeight +\* \/ chmB!UpdateClient(storeChainA.latestHeight) +\* /\ UNCHANGED<> (* Relaying action for the environment. @@ -191,9 +191,12 @@ RelayNextEnv == ELSE storeChainA.latestHeight IN /\ RelayMessage(outBufChainA, inBufChainB) (* TODO: remove following line to fix the deadlock. *) -\* /\ chmB!UpdateClient(storeChainA.latestHeight) - /\ \/ chmB!CanAdvance /\ chmB!UpdateClient(targetHeight) - \/ ~ chmB!CanAdvance /\ UNCHANGED storeChainB + /\ chmB!UpdateClient(storeChainA.latestHeight) + /\ \/ chmB!CanAdvance /\ chmB!CanUpdateClient(targetHeight) + /\ chmB!UpdateClient(targetHeight) + \/ (~ chmB!CanAdvance \/ ~ chmB!CanUpdateClient(targetHeight)) +\* /\ \/ chmB!CanAdvance /\ chmB!UpdateClient(targetHeight) +\* \/ ~ chmB!CanAdvance /\ UNCHANGED storeChainB /\ UNCHANGED<> \/ LET msg == Head(outBufChainB) targetHeight == IF MessageTypeIncludesConnProof(msg.type) @@ -201,9 +204,10 @@ RelayNextEnv == ELSE storeChainB.latestHeight IN /\ RelayMessage(outBufChainB, inBufChainA) (* TODO: remove following line to fix the deadlock. *) -\* /\ chmA!UpdateClient(storeChainB.latestHeight) - /\ \/ chmA!CanAdvance /\ chmA!UpdateClient(targetHeight) - \/ ~ chmA!CanAdvance /\ UNCHANGED storeChainA + /\ chmA!UpdateClient(storeChainB.latestHeight) + /\ \/ chmA!CanAdvance /\ chmA!CanUpdateClient(targetHeight) + /\ chmA!UpdateClient(targetHeight) + \/ (~ chmA!CanAdvance \/ ~ chmA!CanUpdateClient(targetHeight)) /\ UNCHANGED<> @@ -218,7 +222,7 @@ RelayNextEnv == *) NextEnv == - \/ DefaultNextEnv +\* \/ DefaultNextEnv \/ RelayNextEnv @@ -324,6 +328,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Wed May 20 19:42:17 CEST 2020 by adi +\* Last modified Thu May 21 08:03:07 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From 28bc5d9105d20b2886fd1644c11c45fd488b8e68 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 21 May 2020 11:05:31 +0200 Subject: [PATCH 58/63] Cleaned version, ready for #58 --- .../ConnectionHandshakeModule.tla | 26 ++++----- .../spec/connection-handshake/Environment.tla | 57 +++++++++---------- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla index 7908dfab58..329d4c97cc 100644 --- a/verification/spec/connection-handshake/ConnectionHandshakeModule.tla +++ b/verification/spec/connection-handshake/ConnectionHandshakeModule.tla @@ -315,22 +315,20 @@ CanAdvance == store.latestHeight < MaxChainHeight -(* Action for updating the local client on this chain with a height. - - The environment triggers this as part of the GoodNextEnv action. +(* Action for updating the local client on this chain with a new height. + This primes the store; leaves the chain buffers unchanged. This will also advance the chain height. *) -UpdateClient(height) == - \/ /\ height \notin store.client.consensusHeights - (* Warning: following line should provoke a deadlock in ICS3 protocol. *) - /\ height >= store.client.latestHeight - /\ store' = [store EXCEPT !.latestHeight = @ + 1, - !.client.consensusHeights = @ \cup {height}, - !.client.latestHeight = MAX(height, store.client.latestHeight)] - \/ /\ height \in store.client.consensusHeights - /\ UNCHANGED store - +UpdateClient(height) == + /\ store' = [store EXCEPT !.latestHeight = @ + 1, + !.client.consensusHeights = @ \cup {height}, + !.client.latestHeight = MAX(height, store.client.latestHeight)] + +CanUpdateClient(height) == + /\ CanAdvance + /\ height \notin store.client.consensusHeights + (* Generic action for handling any type of inbound message. @@ -378,6 +376,6 @@ TypeInvariant == ============================================================================= \* Modification History -\* Last modified Wed May 20 16:27:30 CEST 2020 by adi +\* Last modified Thu May 21 09:22:33 CEST 2020 by adi \* Created Fri Apr 24 19:08:19 CEST 2020 by adi diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index e355cd3585..6c9acf6def 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -37,7 +37,6 @@ VARIABLES storeChainA, \* The local store of chain A. storeChainB \* The local store of chain B. - (************* ChainAConnectionEnds & ChainBConnectionEnds ***************** The set of records that each chain can use as a valid local connection @@ -166,16 +165,15 @@ RelayMessage(from, to) == or B by advancing the height or updating the client on that chain. *) -\*DefaultNextEnv == -\* \/ /\ chmA!CanAdvance -\* /\ \/ chmA!AdvanceChainHeight -\* \/ chmA!UpdateClient(storeChainB.latestHeight) -\* /\ UNCHANGED<> -\* \/ /\ chmB!CanAdvance -\* /\ \/ chmB!AdvanceChainHeight -\* \/ chmB!UpdateClient(storeChainA.latestHeight) -\* /\ UNCHANGED<> - +DefaultNextEnv == + \/ /\ chmA!CanAdvance + /\ \/ chmA!AdvanceChainHeight + \/ chmA!UpdateClient(storeChainB.latestHeight) + /\ UNCHANGED<> + \/ /\ chmB!CanAdvance + /\ \/ chmB!AdvanceChainHeight + \/ chmB!UpdateClient(storeChainA.latestHeight) + /\ UNCHANGED<> (* Relaying action for the environment. @@ -190,24 +188,20 @@ RelayNextEnv == THEN msg.proofHeight ELSE storeChainA.latestHeight IN /\ RelayMessage(outBufChainA, inBufChainB) - (* TODO: remove following line to fix the deadlock. *) - /\ chmB!UpdateClient(storeChainA.latestHeight) - /\ \/ chmB!CanAdvance /\ chmB!CanUpdateClient(targetHeight) - /\ chmB!UpdateClient(targetHeight) - \/ (~ chmB!CanAdvance \/ ~ chmB!CanUpdateClient(targetHeight)) -\* /\ \/ chmB!CanAdvance /\ chmB!UpdateClient(targetHeight) -\* \/ ~ chmB!CanAdvance /\ UNCHANGED storeChainB + /\ \/ chmB!CanUpdateClient(targetHeight) + /\ chmB!UpdateClient(targetHeight) + \/ ~ chmB!CanUpdateClient(targetHeight) + /\ UNCHANGED storeChainB /\ UNCHANGED<> \/ LET msg == Head(outBufChainB) targetHeight == IF MessageTypeIncludesConnProof(msg.type) THEN msg.proofHeight ELSE storeChainB.latestHeight IN /\ RelayMessage(outBufChainB, inBufChainA) - (* TODO: remove following line to fix the deadlock. *) - /\ chmA!UpdateClient(storeChainB.latestHeight) - /\ \/ chmA!CanAdvance /\ chmA!CanUpdateClient(targetHeight) - /\ chmA!UpdateClient(targetHeight) - \/ (~ chmA!CanAdvance \/ ~ chmA!CanUpdateClient(targetHeight)) + /\ \/ chmA!CanUpdateClient(targetHeight) + /\ chmA!UpdateClient(targetHeight) + \/ ~ chmA!CanUpdateClient(targetHeight) + /\ UNCHANGED storeChainA /\ UNCHANGED<> @@ -215,14 +209,15 @@ RelayNextEnv == There are two possible actions that the environment may perform: - 1. A default step: the environment advances or updates the client on - one of the two chains. + 1. An update client step: the environment updates the client on + one of the two chains, if that chain is expecting to receive a message + (i.e., if there is a message in the ougoing buffer of the other chain). - 2. The environment performs a relaying step. + 2. Otherwise, the environment performs a relaying step. *) NextEnv == -\* \/ DefaultNextEnv + \/ DefaultNextEnv \/ RelayNextEnv @@ -275,7 +270,7 @@ Next == \/ ICS3ReachedOpenConnection \/ ICS3ImpossibleToAdvance \/ NextEnv - \/ chmA!Next /\ UNCHANGED chainBVars + \/ chmA!Next /\ UNCHANGED chainBVars \/ chmB!Next /\ UNCHANGED chainAVars @@ -306,8 +301,8 @@ TypeInvariant == Termination == []((/\ storeChainA.latestHeight < MaxHeight - 4 /\ storeChainB.latestHeight < MaxHeight - 4) - => <> (/\ storeChainA.connection.state = "OPEN" - /\ storeChainB.connection.state = "OPEN")) + => <> [](/\ storeChainA.connection.state = "OPEN" + /\ storeChainB.connection.state = "OPEN")) (* Safety property. @@ -328,6 +323,6 @@ Consistency == ============================================================================= \* Modification History -\* Last modified Thu May 21 08:03:07 CEST 2020 by adi +\* Last modified Thu May 21 09:19:12 CEST 2020 by adi \* Created Fri Apr 24 18:51:07 CEST 2020 by adi From d0a08c19a1d25594b03f338db00c1fc150d1ec8a Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 21 May 2020 11:20:14 +0200 Subject: [PATCH 59/63] Updated readme.md --- verification/spec/connection-handshake/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/verification/spec/connection-handshake/README.md b/verification/spec/connection-handshake/README.md index 107f0d5e9c..3cc273b189 100644 --- a/verification/spec/connection-handshake/README.md +++ b/verification/spec/connection-handshake/README.md @@ -2,27 +2,28 @@ This is a high-level TLA+ spec for the IBC Connection Handshake (CH) protocol. -The spec has two modules: +The spec has three modules: - `Environment.tla` (main model lives here) - - `ConnectionHandshakeModule.tla` (the spec for the IBC CH module) + - `ConnectionHandshakeModule.tla` (the spec for the ICS3 module) + - `ICS3Types.tla` (common domain definitions) To run this: -1. add the two modules in a new specification in the toolbox +1. add the three modules in a new specification in the toolbox 2. specify values for constants MaxHeight and MaxBufLen Note the assumptions: ``` ASSUME MaxHeight > 1 -ASSUME MaxBufLen > 1 +ASSUME MaxBufLen >= 1 ``` Typical values could be: `MaxHeight = 5` and `MaxBufLen = 2`. -3. add the invariant `ConsistencyInv` and the property (temporal formula) `Termination` +3. add the invariant `ConsistencyInv` and `TypeInvariant` as well as the property (temporal formula) `Termination`. 4. run the model checker. \ No newline at end of file From b349d322850f8a634d796b2b38d8913c59cfcd19 Mon Sep 17 00:00:00 2001 From: Adi Seredinschi Date: Thu, 21 May 2020 11:22:37 +0200 Subject: [PATCH 60/63] Updated readme.md typos --- verification/spec/connection-handshake/Environment.tla | 2 +- verification/spec/connection-handshake/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/verification/spec/connection-handshake/Environment.tla b/verification/spec/connection-handshake/Environment.tla index 6c9acf6def..0b0bb87f8b 100644 --- a/verification/spec/connection-handshake/Environment.tla +++ b/verification/spec/connection-handshake/Environment.tla @@ -5,7 +5,7 @@ This module is part of the TLA+ specification for the IBC Connection Handshake protocol (identifier 'ICS3'). This is a high-level spec of ICS3. - This module captures the operators and actions outside of the CH protocol + This module captures the operators and actions outside of the ICS3 protocol itself (i.e., the environment). Among others, the environment does the following: - creates two instances of ConnectionHandshakeModule, diff --git a/verification/spec/connection-handshake/README.md b/verification/spec/connection-handshake/README.md index 3cc273b189..ec22ca6fb4 100644 --- a/verification/spec/connection-handshake/README.md +++ b/verification/spec/connection-handshake/README.md @@ -1,7 +1,7 @@ -# IBC Connection Handshake (CH) TLA+ spec +# IBC Connection Handshake (ICS3) TLA+ spec -This is a high-level TLA+ spec for the IBC Connection Handshake (CH) protocol. +This is a high-level TLA+ spec for the IBC Connection Handshake (ICS3) protocol. The spec has three modules: - `Environment.tla` (main model lives here) From 8feb8dc67d217dd2d2209f767464147d4d50941a Mon Sep 17 00:00:00 2001 From: Anca Zamfir Date: Mon, 25 May 2020 07:21:26 +0200 Subject: [PATCH 61/63] Implement channel query and ChanOpenInit message (#65) * Add channel module and query * Add MsgChanOpenInit message * Added more tests and error handling Use of 'map_err' to convert errors when parsing Return Resuls for constructor functions for channel and endpoint, and perform validation * Fixed connection hops validation Fixed some clippy errors Allow too_many_arguments for message parameters * review comments --- modules/src/ics02_client/msgs.rs | 2 +- modules/src/ics02_client/query.rs | 2 +- modules/src/ics02_client/state.rs | 2 +- modules/src/ics04_channel/channel.rs | 107 +++++++++ modules/src/ics04_channel/error.rs | 28 +++ modules/src/ics04_channel/exported.rs | 194 ++++++++++++++++ modules/src/ics04_channel/mod.rs | 7 + modules/src/ics04_channel/msgs.rs | 217 ++++++++++++++++++ modules/src/ics04_channel/query.rs | 101 ++++++++ modules/src/ics07_tendermint/client_state.rs | 2 +- .../ics07_tendermint/msgs/create_client.rs | 2 +- .../ics07_tendermint/msgs/update_client.rs | 2 +- modules/src/ics24_host/client.rs | 35 --- modules/src/ics24_host/identifier.rs | 125 ++++++++++ modules/src/ics24_host/mod.rs | 2 +- modules/src/ics24_host/validate.rs | 26 ++- modules/src/lib.rs | 1 + modules/src/path.rs | 26 ++- modules/src/path/cosmos.rs | 6 +- modules/src/path/ics.rs | 14 +- relayer/cli/src/commands/query.rs | 17 +- relayer/cli/src/commands/query/channel.rs | 107 +++++++++ relayer/cli/src/commands/query/client.rs | 2 +- relayer/relay/src/query.rs | 1 + relayer/relay/src/query/channel.rs | 22 ++ relayer/relay/src/query/client.rs | 2 +- 26 files changed, 997 insertions(+), 55 deletions(-) create mode 100644 modules/src/ics04_channel/channel.rs create mode 100644 modules/src/ics04_channel/error.rs create mode 100644 modules/src/ics04_channel/exported.rs create mode 100644 modules/src/ics04_channel/mod.rs create mode 100644 modules/src/ics04_channel/msgs.rs create mode 100644 modules/src/ics04_channel/query.rs delete mode 100644 modules/src/ics24_host/client.rs create mode 100644 modules/src/ics24_host/identifier.rs create mode 100644 relayer/cli/src/commands/query/channel.rs create mode 100644 relayer/relay/src/query/channel.rs diff --git a/modules/src/ics02_client/msgs.rs b/modules/src/ics02_client/msgs.rs index b9123fc1d5..2e8bd92600 100644 --- a/modules/src/ics02_client/msgs.rs +++ b/modules/src/ics02_client/msgs.rs @@ -1,7 +1,7 @@ use super::client_type::ClientType; use super::header::Header; use super::state::ConsensusState; -use crate::ics24_host::client::ClientId; +use crate::ics24_host::identifier::ClientId; // FIXME: Remove Header associated type and use Box instead of // Self::Header? diff --git a/modules/src/ics02_client/query.rs b/modules/src/ics02_client/query.rs index 3b83dff054..fcee6dec9f 100644 --- a/modules/src/ics02_client/query.rs +++ b/modules/src/ics02_client/query.rs @@ -7,7 +7,7 @@ use crate::ics23_commitment::{CommitmentPath, CommitmentProof}; use crate::error; use crate::ics02_client::state::{ClientState, ConsensusState}; -use crate::ics24_host::client::ClientId; +use crate::ics24_host::identifier::ClientId; use crate::path::{ClientStatePath, ConsensusStatePath, Path}; use crate::query::{IbcQuery, IbcResponse}; use crate::Height; diff --git a/modules/src/ics02_client/state.rs b/modules/src/ics02_client/state.rs index fed45bcd4f..926776eec3 100644 --- a/modules/src/ics02_client/state.rs +++ b/modules/src/ics02_client/state.rs @@ -1,7 +1,7 @@ use super::client_type::ClientType; use crate::ics23_commitment::CommitmentRoot; -use crate::ics24_host::client::ClientId; +use crate::ics24_host::identifier::ClientId; use crate::Height; pub trait ConsensusState { diff --git a/modules/src/ics04_channel/channel.rs b/modules/src/ics04_channel/channel.rs new file mode 100644 index 0000000000..f8dcbf1719 --- /dev/null +++ b/modules/src/ics04_channel/channel.rs @@ -0,0 +1,107 @@ +use super::exported::*; +use crate::ics04_channel::error; +use crate::ics04_channel::error::Kind; +use crate::ics24_host::identifier::{ChannelId, ConnectionId, PortId}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Channel { + state: State, + ordering: Order, + remote: Endpoint, + connection_hops: Vec, + version: String, +} + +impl Channel { + pub fn new( + ordering: Order, + remote: Endpoint, + connection_hops: Vec, + version: String, + ) -> Self { + Self { + state: State::Uninitialized, + ordering, + remote, + connection_hops, + version, + } + } +} + +impl ChannelI for Channel { + type ValidationError = crate::ics04_channel::error::Error; + + fn state(&self) -> State { + self.state.clone() + } + + fn ordering(&self) -> Order { + self.ordering.clone() + } + + fn counterparty(&self) -> Box> { + Box::new(self.remote.clone()) + } + + fn connection_hops(&self) -> Vec { + self.connection_hops.clone() + } + + fn version(&self) -> String { + self.version.parse().unwrap() + } + + fn validate_basic(&self) -> Result<(), Self::ValidationError> { + if self.connection_hops.len() != 1 { + return Err(error::Kind::InvalidConnectionHopsLength + .context("validate channel") + .into()); + } + if self.version().trim() == "" { + return Err(error::Kind::InvalidVersion + .context("empty version string") + .into()); + } + self.counterparty().validate_basic() + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Endpoint { + port_id: PortId, + channel_id: ChannelId, +} + +impl Endpoint { + pub fn new( + port_id: String, + channel_id: String, + ) -> Result { + Ok(Self { + port_id: port_id + .parse() + .map_err(|e| Kind::IdentifierError.context(e))?, + channel_id: channel_id + .parse() + .map_err(|e| Kind::IdentifierError.context(e))?, + }) + } +} + +impl CounterpartyI for Endpoint { + type ValidationError = crate::ics04_channel::error::Error; + + fn port_id(&self) -> String { + String::from(self.port_id.as_str()) + } + + fn channel_id(&self) -> String { + String::from(self.channel_id.as_str()) + } + + fn validate_basic(&self) -> Result<(), Self::ValidationError> { + Ok(()) + } +} diff --git a/modules/src/ics04_channel/error.rs b/modules/src/ics04_channel/error.rs new file mode 100644 index 0000000000..a366862883 --- /dev/null +++ b/modules/src/ics04_channel/error.rs @@ -0,0 +1,28 @@ +use anomaly::{BoxError, Context}; +use thiserror::Error; + +pub type Error = anomaly::Error; + +#[derive(Clone, Debug, Error)] +pub enum Kind { + #[error("channel state unknown")] + UnknownState, + + #[error("identifier error")] + IdentifierError, + + #[error("channel order type unknown")] + UnknownOrderType, + + #[error("invalid connection hops length")] + InvalidConnectionHopsLength, + + #[error("invalid version")] + InvalidVersion, +} + +impl Kind { + pub fn context(self, source: impl Into) -> Context { + Context::new(self, Some(source.into())) + } +} diff --git a/modules/src/ics04_channel/exported.rs b/modules/src/ics04_channel/exported.rs new file mode 100644 index 0000000000..17d962f288 --- /dev/null +++ b/modules/src/ics04_channel/exported.rs @@ -0,0 +1,194 @@ +use super::error; +use crate::ics24_host::identifier::ConnectionId; +use anomaly::fail; +use serde_derive::{Deserialize, Serialize}; + +pub trait ChannelI { + type ValidationError: std::error::Error; + fn state(&self) -> State; + fn ordering(&self) -> Order; + fn counterparty(&self) -> Box>; + fn connection_hops(&self) -> Vec; + fn version(&self) -> String; + fn validate_basic(&self) -> Result<(), Self::ValidationError>; +} + +pub trait CounterpartyI { + type ValidationError: std::error::Error; + fn port_id(&self) -> String; + fn channel_id(&self) -> String; + fn validate_basic(&self) -> Result<(), Self::ValidationError>; +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum State { + Uninitialized = 0, + Init, + TryOpen, + Open, + Closed, +} + +impl State { + /// Yields the state as a string + pub fn as_string(&self) -> &'static str { + match self { + Self::Uninitialized => "UNINITIALIZED", + Self::Init => "INIT", + Self::TryOpen => "TRYOPEN", + Self::Open => "OPEN", + Self::Closed => "CLOSED", + } + } +} + +impl std::str::FromStr for State { + type Err = error::Error; + + fn from_str(s: &str) -> Result { + match s { + "UNINITIALIZED" => Ok(Self::Uninitialized), + "INIT" => Ok(Self::Init), + "TRYOPEN" => Ok(Self::TryOpen), + "OPEN" => Ok(Self::Open), + "CLOSED" => Ok(Self::Closed), + _ => fail!(error::Kind::UnknownState, s), + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum Order { + None = 0, + Unordered, + Ordered, +} + +impl Order { + /// Yields the Order as a string + pub fn as_string(&self) -> &'static str { + match self { + Self::None => "UNINITIALIZED", + Self::Unordered => "UNORDERED", + Self::Ordered => "ORDERED", + } + } +} + +impl std::str::FromStr for Order { + type Err = error::Error; + + fn from_str(s: &str) -> Result { + match s { + "UNINITIALIZED" => Ok(Self::None), + "UNORDERED" => Ok(Self::Unordered), + "ORDERED" => Ok(Self::Ordered), + _ => fail!(error::Kind::UnknownOrderType, s), + } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + #[test] + fn parse_channel_ordering_type() { + use super::Order; + + struct Test { + ordering: &'static str, + want_res: Order, + want_err: bool, + } + let tests: Vec = vec![ + Test { + ordering: "UNINITIALIZED", + want_res: Order::None, + want_err: false, + }, + Test { + ordering: "UNORDERED", + want_res: Order::Unordered, + want_err: false, + }, + Test { + ordering: "ORDERED", + want_res: Order::Ordered, + want_err: false, + }, + Test { + ordering: "UNKNOWN_ORDER", + want_res: Order::None, + want_err: true, + }, + ] + .into_iter() + .collect(); + + for test in tests { + match Order::from_str(test.ordering) { + Ok(res) => { + assert!(!test.want_err); + assert_eq!(test.want_res, res); + } + Err(_) => assert!(test.want_err, "parse failed"), + } + } + } + + #[test] + fn parse_channel_state() { + use super::State; + + struct Test { + state: &'static str, + want_res: State, + want_err: bool, + } + let tests: Vec = vec![ + Test { + state: "UNINITIALIZED", + want_res: State::Uninitialized, + want_err: false, + }, + Test { + state: "INIT", + want_res: State::Init, + want_err: false, + }, + Test { + state: "TRYOPEN", + want_res: State::TryOpen, + want_err: false, + }, + Test { + state: "OPEN", + want_res: State::Open, + want_err: false, + }, + Test { + state: "CLOSED", + want_res: State::Closed, + want_err: false, + }, + Test { + state: "INVALID_STATE", + want_res: State::Open, + want_err: true, + }, + ] + .into_iter() + .collect(); + + for test in tests { + match State::from_str(test.state) { + Ok(res) => { + assert!(!test.want_err); + assert_eq!(test.want_res, res); + } + Err(_) => assert!(test.want_err, "parse failed"), + } + } + } +} diff --git a/modules/src/ics04_channel/mod.rs b/modules/src/ics04_channel/mod.rs new file mode 100644 index 0000000000..6c69b989b5 --- /dev/null +++ b/modules/src/ics04_channel/mod.rs @@ -0,0 +1,7 @@ +//! ICS 04: IBC Channel implementation + +pub mod channel; +pub mod error; +pub mod exported; +pub mod msgs; +pub mod query; diff --git a/modules/src/ics04_channel/msgs.rs b/modules/src/ics04_channel/msgs.rs new file mode 100644 index 0000000000..a534360b56 --- /dev/null +++ b/modules/src/ics04_channel/msgs.rs @@ -0,0 +1,217 @@ +#![allow(clippy::too_many_arguments)] +use super::channel::{Channel, Endpoint}; +use super::exported::*; +use crate::ics04_channel::error::Kind; +use crate::ics24_host::identifier::{ChannelId, ConnectionId, PortId}; +use serde_derive::{Deserialize, Serialize}; +use std::str::FromStr; +use tendermint::account::Id as AccountId; + +pub const TYPE_MSG_CHANNEL_OPEN_INIT: &str = "channel_open_init"; + +// FIXME - this should be common for all messages not just channel, client also defines it. +pub trait Msg { + type ValidationError: std::error::Error; + + fn route(&self) -> String; + + fn get_type(&self) -> String; + + fn validate_basic(&self) -> Result<(), Self::ValidationError>; + + fn get_sign_bytes(&self) -> Vec; + + fn get_signers(&self) -> Vec; +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MsgChannelOpenInit { + port_id: PortId, + channel_id: ChannelId, + channel: Channel, + signer: AccountId, +} + +impl MsgChannelOpenInit { + pub fn new( + port_id: String, + channel_id: String, + version: String, + order: String, + connection_hops: Vec, + counterparty_port_id: String, + counterparty_channel_id: String, + signer: AccountId, + ) -> Result { + let connection_hops: Result, _> = connection_hops + .into_iter() + .map(|s| ConnectionId::from_str(s.as_str())) + .collect(); + + Ok(Self { + port_id: port_id + .parse() + .map_err(|e| Kind::IdentifierError.context(e))?, + channel_id: channel_id + .parse() + .map_err(|e| Kind::IdentifierError.context(e))?, + channel: Channel::new( + order.parse()?, + Endpoint::new(counterparty_port_id, counterparty_channel_id) + .map_err(|e| Kind::IdentifierError.context(e))?, + connection_hops.map_err(|e| Kind::IdentifierError.context(e))?, + version, + ), + signer, + }) + } +} + +impl Msg for MsgChannelOpenInit { + type ValidationError = crate::ics04_channel::error::Error; + + fn route(&self) -> String { + crate::keys::ROUTER_KEY.to_string() + } + + fn get_type(&self) -> String { + TYPE_MSG_CHANNEL_OPEN_INIT.to_string() + } + + fn validate_basic(&self) -> Result<(), Self::ValidationError> { + self.channel.validate_basic() + } + + fn get_sign_bytes(&self) -> Vec { + todo!() + } + + fn get_signers(&self) -> Vec { + vec![self.signer] + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use tendermint::account::Id as AccountId; + + #[test] + fn parse_channel_open_init_msg() { + use super::MsgChannelOpenInit; + let id_hex = "0CDA3F47EF3C4906693B170EF650EB968C5F4B2C"; + let acc = AccountId::from_str(id_hex).unwrap(); + + #[derive(Clone, Debug, PartialEq)] + struct OpenInitParams { + port_id: String, + channel_id: String, + version: String, + order: String, + connection_hops: Vec, + counterparty_port_id: String, + counterparty_channel_id: String, + } + + let default_params = OpenInitParams { + port_id: "port".to_string(), + channel_id: "testchannel".to_string(), + version: "1.0".to_string(), + order: "ORDERED".to_string(), + connection_hops: vec!["connectionhop".to_string()].into_iter().collect(), + counterparty_port_id: "destport".to_string(), + counterparty_channel_id: "testdestchannel".to_string(), + }; + + struct Test { + name: String, + params: OpenInitParams, + want_pass: bool, + } + + let tests: Vec = vec![ + Test { + name: "Good parameters".to_string(), + params: default_params.clone(), + want_pass: true, + }, + Test { + name: "Bad port, non-alpha".to_string(), + params: OpenInitParams { + port_id: "p34".to_string(), + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Bad channel, name too short".to_string(), + params: OpenInitParams { + channel_id: "connone".to_string(), + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Bad version".to_string(), + params: OpenInitParams { + version: " . ".to_string(), + ..default_params.clone() + }, + want_pass: true, // verified in validate_basic() + }, + Test { + name: "Bad order".to_string(), + params: OpenInitParams { + order: "MYORER".to_string(), + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Bad connection hops".to_string(), + params: OpenInitParams { + connection_hops: vec!["conn124".to_string()].into_iter().collect(), + ..default_params.clone() + }, + want_pass: false, + }, + ] + .into_iter() + .collect(); + + for test in tests { + let p = test.params.clone(); + + let msg = MsgChannelOpenInit::new( + p.port_id, + p.channel_id, + p.version, + p.order, + p.connection_hops, + p.counterparty_port_id, + p.counterparty_channel_id, + acc, + ); + + match msg { + Ok(_res) => { + assert!( + test.want_pass, + "MsgChanOpenInit::new should have failed for test {}, \nmsg {:?}", + test.name, + test.params.clone() + ); + } + Err(err) => { + assert!( + !test.want_pass, + "MsgChanOpenInit::new failed for test {}, \nmsg {:?} with err {:?}", + test.name, + test.params.clone(), + err + ); + } + } + } + } +} diff --git a/modules/src/ics04_channel/query.rs b/modules/src/ics04_channel/query.rs new file mode 100644 index 0000000000..55a4822e22 --- /dev/null +++ b/modules/src/ics04_channel/query.rs @@ -0,0 +1,101 @@ +use tendermint::rpc::endpoint::abci_query::AbciQuery; + +use tendermint::abci; + +use crate::ics23_commitment::{CommitmentPath, CommitmentProof}; + +use crate::error; +use crate::ics04_channel::channel::Channel; +use crate::ics24_host::identifier::{ChannelId, PortId}; +use crate::path::{ChannelEndsPath, Path}; +use crate::query::{IbcQuery, IbcResponse}; +use crate::Height; + +pub struct QueryChannel { + pub chain_height: Height, + pub port_id: PortId, + pub channel_id: ChannelId, + pub channel_ends_path: ChannelEndsPath, + pub prove: bool, +} + +impl QueryChannel { + pub fn new(chain_height: Height, port_id: PortId, channel_id: ChannelId, prove: bool) -> Self { + Self { + chain_height, + port_id: port_id.clone(), + channel_id: channel_id.clone(), + channel_ends_path: ChannelEndsPath::new(port_id, channel_id), + prove, + } + } +} + +impl IbcQuery for QueryChannel { + type Response = ChannelResponse; + + fn path(&self) -> abci::Path { + "/store/ibc/key".parse().unwrap() + } + + fn height(&self) -> Height { + self.chain_height + } + + fn prove(&self) -> bool { + self.prove + } + + fn data(&self) -> Vec { + self.channel_ends_path.to_key().into() + } +} + +pub struct ChannelResponse { + pub channel: Channel, + pub proof: Option, + pub proof_path: CommitmentPath, + pub proof_height: Height, +} + +impl ChannelResponse { + pub fn new( + port_id: PortId, + channel_id: ChannelId, + channel: Channel, + abci_proof: Option, + proof_height: Height, + ) -> Self { + let proof_path = CommitmentPath::from_path(ChannelEndsPath::new(port_id, channel_id)); + + ChannelResponse { + channel, + proof: abci_proof, + proof_path, + proof_height, + } + } +} + +impl IbcResponse for ChannelResponse { + fn from_abci_response(query: QueryChannel, response: AbciQuery) -> Result { + match (response.value, &response.proof) { + (Some(value), _) => { + let channel = amino_unmarshal_binary_length_prefixed(&value)?; + + Ok(ChannelResponse::new( + query.port_id, + query.channel_id, + channel, + response.proof, + response.height.into(), + )) + } + (None, _) => Err(error::Kind::Rpc.context("Bad response").into()), + } + } +} + +fn amino_unmarshal_binary_length_prefixed(_bytes: &[u8]) -> Result { + todo!() +} diff --git a/modules/src/ics07_tendermint/client_state.rs b/modules/src/ics07_tendermint/client_state.rs index 23a62474ff..f026d45881 100644 --- a/modules/src/ics07_tendermint/client_state.rs +++ b/modules/src/ics07_tendermint/client_state.rs @@ -2,7 +2,7 @@ use crate::ics02_client::client_type::ClientType; use crate::ics23_commitment::CommitmentRoot; use crate::ics07_tendermint::header::Header; -use crate::ics24_host::client::ClientId; +use crate::ics24_host::identifier::ClientId; use serde_derive::{Deserialize, Serialize}; use std::time::Duration; use tendermint::lite::Header as liteHeader; diff --git a/modules/src/ics07_tendermint/msgs/create_client.rs b/modules/src/ics07_tendermint/msgs/create_client.rs index d7c085277b..36f0c42200 100644 --- a/modules/src/ics07_tendermint/msgs/create_client.rs +++ b/modules/src/ics07_tendermint/msgs/create_client.rs @@ -3,7 +3,7 @@ use crate::ics02_client::msgs::Msg; use crate::ics07_tendermint::consensus_state::ConsensusState; use crate::ics07_tendermint::header::Header; use crate::ics23_commitment::CommitmentRoot; -use crate::ics24_host::client::ClientId; +use crate::ics24_host::identifier::ClientId; use serde_derive::{Deserialize, Serialize}; use tendermint::account::Id as AccountId; diff --git a/modules/src/ics07_tendermint/msgs/update_client.rs b/modules/src/ics07_tendermint/msgs/update_client.rs index 8d13acf383..7ce625317c 100644 --- a/modules/src/ics07_tendermint/msgs/update_client.rs +++ b/modules/src/ics07_tendermint/msgs/update_client.rs @@ -1,6 +1,6 @@ use crate::ics02_client::msgs::Msg; use crate::ics07_tendermint::header::Header; -use crate::ics24_host::client::ClientId; +use crate::ics24_host::identifier::ClientId; use serde_derive::{Deserialize, Serialize}; use tendermint::account::Id as AccountId; diff --git a/modules/src/ics24_host/client.rs b/modules/src/ics24_host/client.rs deleted file mode 100644 index 1554be9eac..0000000000 --- a/modules/src/ics24_host/client.rs +++ /dev/null @@ -1,35 +0,0 @@ -use serde_derive::{Deserialize, Serialize}; -use std::str::FromStr; - -use super::error::ValidationError; -use super::validate::validate_client_identifier; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct ClientId(String); - -impl ClientId { - /// Get this identifier as a borrowed `&str` - pub fn as_str(&self) -> &str { - &self.0 - } - - /// Get this identifier as a borrowed byte slice - pub fn as_bytes(&self) -> &[u8] { - &self.0.as_bytes() - } -} - -/// This implementation provides a `to_string` method. -impl std::fmt::Display for ClientId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.0) - } -} - -impl FromStr for ClientId { - type Err = ValidationError; - - fn from_str(s: &str) -> Result { - validate_client_identifier(s).map(|_| Self(s.to_string())) - } -} diff --git a/modules/src/ics24_host/identifier.rs b/modules/src/ics24_host/identifier.rs new file mode 100644 index 0000000000..7f03c756cd --- /dev/null +++ b/modules/src/ics24_host/identifier.rs @@ -0,0 +1,125 @@ +use serde_derive::{Deserialize, Serialize}; +use std::str::FromStr; + +use super::error::ValidationError; +use super::validate::*; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct ClientId(String); + +impl ClientId { + /// Get this identifier as a borrowed `&str` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get this identifier as a borrowed byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.0.as_bytes() + } +} + +/// This implementation provides a `to_string` method. +impl std::fmt::Display for ClientId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) + } +} + +impl FromStr for ClientId { + type Err = ValidationError; + + fn from_str(s: &str) -> Result { + validate_client_identifier(s).map(|_| Self(s.to_string())) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct ConnectionId(String); + +impl ConnectionId { + /// Get this identifier as a borrowed `&str` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get this identifier as a borrowed byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.0.as_bytes() + } +} + +/// This implementation provides a `to_string` method. +impl std::fmt::Display for ConnectionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) + } +} + +impl FromStr for ConnectionId { + type Err = ValidationError; + + fn from_str(s: &str) -> Result { + validate_connection_identifier(s).map(|_| Self(s.to_string())) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct PortId(String); + +impl PortId { + /// Get this identifier as a borrowed `&str` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get this identifier as a borrowed byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.0.as_bytes() + } +} + +/// This implementation provides a `to_string` method. +impl std::fmt::Display for PortId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) + } +} + +impl FromStr for PortId { + type Err = ValidationError; + + fn from_str(s: &str) -> Result { + validate_port_identifier(s).map(|_| Self(s.to_string())) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct ChannelId(String); + +impl ChannelId { + /// Get this identifier as a borrowed `&str` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get this identifier as a borrowed byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.0.as_bytes() + } +} + +/// This implementation provides a `to_string` method. +impl std::fmt::Display for ChannelId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) + } +} + +impl FromStr for ChannelId { + type Err = ValidationError; + + fn from_str(s: &str) -> Result { + validate_channel_identifier(s).map(|_| Self(s.to_string())) + } +} diff --git a/modules/src/ics24_host/mod.rs b/modules/src/ics24_host/mod.rs index 9aecc7f58b..e187cd7983 100644 --- a/modules/src/ics24_host/mod.rs +++ b/modules/src/ics24_host/mod.rs @@ -1,5 +1,5 @@ //! ICS 24: Host Requirements -pub mod client; pub mod error; +pub mod identifier; pub mod validate; diff --git a/modules/src/ics24_host/validate.rs b/modules/src/ics24_host/validate.rs index 8bff5652a6..d2a26a37fd 100644 --- a/modules/src/ics24_host/validate.rs +++ b/modules/src/ics24_host/validate.rs @@ -48,8 +48,32 @@ pub fn validate_identifier(id: &str, min: usize, max: usize) -> Result<(), Valid /// Default validator function for Client identifiers. /// -/// A valid identifier must be between 10-20 characters and only contain lowercase +/// A valid identifier must be between 9-20 characters and only contain lowercase /// alphabetic characters, pub fn validate_client_identifier(id: &str) -> Result<(), ValidationError> { + validate_identifier(id, 9, 20) +} + +/// Default validator function for Connection identifiers. +/// +/// A valid Identifier must be between 10-20 characters and only contain lowercase +/// alphabetic characters, +pub fn validate_connection_identifier(id: &str) -> Result<(), ValidationError> { + validate_identifier(id, 10, 20) +} + +/// Default validator function for Port identifiers. +/// +/// A valid Identifier must be between 2-20 characters and only contain lowercase +/// alphabetic characters, +pub fn validate_port_identifier(id: &str) -> Result<(), ValidationError> { + validate_identifier(id, 2, 20) +} + +/// Default validator function for Channel identifiers. +/// +/// A valid Identifier must be between 10-20 characters and only contain lowercase +/// alphabetic characters, +pub fn validate_channel_identifier(id: &str) -> Result<(), ValidationError> { validate_identifier(id, 10, 20) } diff --git a/modules/src/lib.rs b/modules/src/lib.rs index db92fc25ce..a95ed85f2e 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -20,6 +20,7 @@ pub mod error; pub mod ics02_client; +pub mod ics04_channel; pub mod ics07_tendermint; pub mod ics23_commitment; pub mod ics24_host; diff --git a/modules/src/path.rs b/modules/src/path.rs index 17a65ccb14..7a6ec22460 100644 --- a/modules/src/path.rs +++ b/modules/src/path.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::ics24_host::client::ClientId; +use crate::ics24_host::identifier::{ChannelId, ClientId, PortId}; use crate::Height; mod cosmos; @@ -81,3 +81,27 @@ impl Path for ClientStatePath { paths::client_state_path(&self) } } + +pub struct ChannelPath { + pub port_id: PortId, + channel_id: ChannelId, +} + +impl ChannelPath { + pub fn new(port_id: PortId, channel_id: ChannelId) -> Self { + Self { + port_id, + channel_id, + } + } +} + +const KEY_CHANNEL_PREFIX: &str = "channelEnds"; + +pub type ChannelEndsPath = ChannelPath; + +impl Path for ChannelEndsPath { + fn to_string(&self) -> String { + format!("{}/{}", KEY_CHANNEL_PREFIX, paths::channel_path(&self)) + } +} diff --git a/modules/src/path/cosmos.rs b/modules/src/path/cosmos.rs index 84bdde30ce..618b90204a 100644 --- a/modules/src/path/cosmos.rs +++ b/modules/src/path/cosmos.rs @@ -1,4 +1,4 @@ -use super::{ClientStatePath, ConnectionPath, ConsensusStatePath}; +use super::{ChannelPath, ClientStatePath, ConnectionPath, ConsensusStatePath}; pub fn connection_path(path: &ConnectionPath) -> String { format!("connection/{}", path.connection_id) @@ -11,3 +11,7 @@ pub fn consensus_state_path(path: &ConsensusStatePath) -> String { pub fn client_state_path(path: &ClientStatePath) -> String { format!("clientState/{}", path.client_id) } + +pub fn channel_path(path: &ChannelPath) -> String { + format!("ports/{}/channels/{}", path.port_id, path.channel_id) +} diff --git a/modules/src/path/ics.rs b/modules/src/path/ics.rs index 7157c07092..624a8e24ed 100644 --- a/modules/src/path/ics.rs +++ b/modules/src/path/ics.rs @@ -1,8 +1,4 @@ -use super::{ClientStatePath, ConnectionPath, ConsensusStatePath}; - -pub fn connection_path(path: &ConnectionPath) -> String { - format!("connections/{}", path.connection_id) -} +use super::{ChannelPath, ClientStatePath, ConnectionPath, ConsensusStatePath}; pub fn consensus_state_path(path: &ConsensusStatePath) -> String { format!("clients/{}/consensusState/{}", path.client_id, path.height) @@ -11,3 +7,11 @@ pub fn consensus_state_path(path: &ConsensusStatePath) -> String { pub fn client_state_path(path: &ClientStatePath) -> String { format!("clients/{}/clientState", path.client_id) } + +pub fn connection_path(path: &ConnectionPath) -> String { + format!("connections/{}", path.connection_id) +} + +pub fn channel_path(path: &ChannelPath) -> String { + format!("ports/{}/channels/{}", path.port_id, path.channel_id) +} diff --git a/relayer/cli/src/commands/query.rs b/relayer/cli/src/commands/query.rs index af19e2d9de..4d1c20e279 100644 --- a/relayer/cli/src/commands/query.rs +++ b/relayer/cli/src/commands/query.rs @@ -2,6 +2,7 @@ use abscissa_core::{Command, Options, Runnable}; +mod channel; mod client; /// `query` subcommand @@ -10,14 +11,24 @@ pub enum QueryCmd { /// The `query client` subcommand #[options(help = "query client")] Client(QueryClientCmds), + /// The `query channel` subcommand + #[options(help = "query channel")] + Channel(QueryChannelCmds), } #[derive(Command, Debug, Options, Runnable)] pub enum QueryClientCmds { - /// The `query client` subcommand - #[options(help = "query client state")] + /// The `query client state` subcommand + #[options(help = "query client full state")] State(client::QueryClientStateCmd), - + /// The `query client consensus` subcommand #[options(help = "query client consensus")] Consensus(client::QueryClientConsensusCmd), } + +#[derive(Command, Debug, Options, Runnable)] +pub enum QueryChannelCmds { + /// The `query channel ends` subcommand + #[options(help = "query channel ends")] + Ends(channel::QueryChannelEndsCmd), +} diff --git a/relayer/cli/src/commands/query/channel.rs b/relayer/cli/src/commands/query/channel.rs new file mode 100644 index 0000000000..918aece92f --- /dev/null +++ b/relayer/cli/src/commands/query/channel.rs @@ -0,0 +1,107 @@ +use crate::prelude::*; + +use abscissa_core::{Command, Options, Runnable}; +use relayer::config::{ChainConfig, Config}; +use relayer::query::channel::query_channel; + +use relayer_modules::ics24_host::identifier::{ChannelId, PortId}; + +use crate::commands::utils::block_on; +use relayer::chain::tendermint::TendermintChain; +use tendermint::chain::Id as ChainId; + +#[derive(Command, Debug, Options)] +pub struct QueryChannelEndsCmd { + #[options(free, help = "identifier of the chain to query")] + chain_id: Option, + + #[options(free, help = "identifier of the port to query")] + port_id: Option, + + #[options(free, help = "identifier of the channel to query")] + channel_id: Option, + + #[options(help = "height of the state to query", short = "h")] + height: Option, + + #[options(help = "whether proof is required", short = "p")] + proof: Option, +} + +#[derive(Debug)] +struct QueryChannelOptions { + port_id: PortId, + channel_id: ChannelId, + height: u64, + proof: bool, +} + +impl QueryChannelEndsCmd { + fn validate_options( + &self, + config: &Config, + ) -> Result<(ChainConfig, QueryChannelOptions), String> { + match (&self.chain_id, &self.port_id, &self.channel_id) { + (Some(chain_id), Some(port_id), Some(channel_id)) => { + let chain_config = config.chains.iter().find(|c| c.id == *chain_id); + match chain_config { + Some(chain_config) => { + let opts = QueryChannelOptions { + port_id: port_id.parse().unwrap(), + channel_id: channel_id.parse().unwrap(), + height: match self.height { + Some(h) => h, + None => 0 as u64, + }, + proof: match self.proof { + Some(proof) => proof, + None => true, + }, + }; + Ok((chain_config.clone(), opts)) + } + _ => Err(format!("cannot find chain {} in config", chain_id)), + } + } + (None, _, _) => Err("missing chain identifier".to_string()), + (_, None, _) => Err("missing port identifier".to_string()), + (_, _, None) => Err("missing channel identifier".to_string()), + } + } +} + +impl Runnable for QueryChannelEndsCmd { + fn run(&self) { + let config = app_config(); + + let (chain_config, opts) = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + status_info!("Options", "{:?}", opts); + + // run with proof: + // cargo run --bin relayer -- -c simple_config.toml query channel ends ibc0 transfer ibconexfer + // + // run without proof: + // cargo run --bin relayer -- -c simple_config.toml query channel ends ibc0 transfer ibconexfer -p false + // + // Note: currently both fail in amino_unmarshal_binary_length_prefixed(). + // To test this start a Gaia node and configure a channel using the go relayer. + let chain = TendermintChain::from_config(chain_config).unwrap(); + let res = block_on(query_channel( + &chain, + opts.height, + opts.port_id.clone(), + opts.channel_id.clone(), + opts.proof, + )); + match res { + Ok(cs) => status_info!("channel query result: ", "{:?}", cs.channel), + Err(e) => status_info!("channel query error: ", "{:?}", e), + } + } +} diff --git a/relayer/cli/src/commands/query/client.rs b/relayer/cli/src/commands/query/client.rs index 8a61373edc..48b550636c 100644 --- a/relayer/cli/src/commands/query/client.rs +++ b/relayer/cli/src/commands/query/client.rs @@ -4,7 +4,7 @@ use abscissa_core::{Command, Options, Runnable}; use relayer::config::{ChainConfig, Config}; use relayer::query::client::{query_client_consensus_state, query_client_full_state}; -use relayer_modules::ics24_host::client::ClientId; +use relayer_modules::ics24_host::identifier::ClientId; use crate::commands::utils::block_on; use relayer::chain::tendermint::TendermintChain; diff --git a/relayer/relay/src/query.rs b/relayer/relay/src/query.rs index 4df375e684..04132e01c1 100644 --- a/relayer/relay/src/query.rs +++ b/relayer/relay/src/query.rs @@ -4,6 +4,7 @@ use crate::chain::Chain; use relayer_modules::error; use relayer_modules::query::IbcQuery; +pub mod channel; pub mod client; /// Perform an IBC `query` on the given `chain`, and return the corresponding IBC response. diff --git a/relayer/relay/src/query/channel.rs b/relayer/relay/src/query/channel.rs new file mode 100644 index 0000000000..588efbfa53 --- /dev/null +++ b/relayer/relay/src/query/channel.rs @@ -0,0 +1,22 @@ +use relayer_modules::ics24_host::identifier::{ChannelId, PortId}; +use relayer_modules::Height; + +use super::ibc_query; +use crate::chain::Chain; +use relayer_modules::ics04_channel::query::{ChannelResponse, QueryChannel}; + +use relayer_modules::error; + +pub async fn query_channel( + chain: &C, + chain_height: Height, + port_id: PortId, + channel_id: ChannelId, + prove: bool, +) -> Result +where + C: Chain, +{ + let query = QueryChannel::new(chain_height, port_id, channel_id, prove); + ibc_query(chain, query).await +} diff --git a/relayer/relay/src/query/client.rs b/relayer/relay/src/query/client.rs index 438d0bdf3c..74b082c32d 100644 --- a/relayer/relay/src/query/client.rs +++ b/relayer/relay/src/query/client.rs @@ -1,4 +1,4 @@ -use relayer_modules::ics24_host::client::ClientId; +use relayer_modules::ics24_host::identifier::ClientId; use relayer_modules::Height; use super::ibc_query; From e8cf76169682071de29086b92f03153b9057371c Mon Sep 17 00:00:00 2001 From: Anca Zamfir Date: Mon, 25 May 2020 20:46:01 +0200 Subject: [PATCH 62/63] Check query parameters but not against the config (#78) * Check query parameters but not against the config Fixed parameter verification order to be consistent Simplified the code * Fix error string * Added some tests * Fix clippy and test errors tendermint-rs changed the AbciQuery.value to non-option * add forgotten test fixture * attempt to fix codecov * Minor fix in error msg Co-authored-by: Shivani Joshi <46731446+Shivani912@users.noreply.github.com> --- codecov.yml | 1 + modules/src/ics02_client/query.rs | 38 ++--- modules/src/ics04_channel/msgs.rs | 2 +- modules/src/ics04_channel/query.rs | 21 +-- relayer/cli/Cargo.toml | 2 +- relayer/cli/src/commands/query/channel.rs | 174 +++++++++++++++++---- relayer/cli/src/commands/query/client.rs | 133 +++++++++++++--- relayer/cli/tests/fixtures/two_chains.toml | 29 ++++ 8 files changed, 307 insertions(+), 93 deletions(-) create mode 100644 relayer/cli/tests/fixtures/two_chains.toml diff --git a/codecov.yml b/codecov.yml index 9eda9ca38f..a48cba05d6 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,5 +1,6 @@ codecov: require_ci_to_pass: yes + allow_coverage_offsets: true ignore: diff --git a/modules/src/ics02_client/query.rs b/modules/src/ics02_client/query.rs index fcee6dec9f..882e5c39fd 100644 --- a/modules/src/ics02_client/query.rs +++ b/modules/src/ics02_client/query.rs @@ -88,19 +88,12 @@ where query: QueryClientFullState, response: AbciQuery, ) -> Result { - match (response.value, &response.proof) { - (Some(value), _) => { - let client_state = amino_unmarshal_binary_length_prefixed(&value)?; - - Ok(ClientFullStateResponse::new( - query.client_id, - client_state, - response.proof, - response.height.into(), - )) - } - (None, _) => Err(error::Kind::Rpc.context("Bad response").into()), - } + Ok(ClientFullStateResponse::new( + query.client_id, + amino_unmarshal_binary_length_prefixed(&response.value)?, + response.proof, + response.height.into(), + )) } } @@ -192,18 +185,11 @@ where query: QueryClientConsensusState, response: AbciQuery, ) -> Result { - match (response.value, &response.proof) { - (Some(value), _) => { - let consensus_state = amino_unmarshal_binary_length_prefixed(&value)?; - - Ok(ConsensusStateResponse::new( - query.client_id, - consensus_state, - response.proof, - response.height.into(), - )) - } - (None, _) => Err(error::Kind::Rpc.context("Bad response").into()), - } + Ok(ConsensusStateResponse::new( + query.client_id, + amino_unmarshal_binary_length_prefixed(&response.value)?, + response.proof, + response.height.into(), + )) } } diff --git a/modules/src/ics04_channel/msgs.rs b/modules/src/ics04_channel/msgs.rs index a534360b56..aafd8fcefb 100644 --- a/modules/src/ics04_channel/msgs.rs +++ b/modules/src/ics04_channel/msgs.rs @@ -146,7 +146,7 @@ mod tests { Test { name: "Bad channel, name too short".to_string(), params: OpenInitParams { - channel_id: "connone".to_string(), + channel_id: "chshort".to_string(), ..default_params.clone() }, want_pass: false, diff --git a/modules/src/ics04_channel/query.rs b/modules/src/ics04_channel/query.rs index 55a4822e22..f52ace5aa3 100644 --- a/modules/src/ics04_channel/query.rs +++ b/modules/src/ics04_channel/query.rs @@ -79,20 +79,13 @@ impl ChannelResponse { impl IbcResponse for ChannelResponse { fn from_abci_response(query: QueryChannel, response: AbciQuery) -> Result { - match (response.value, &response.proof) { - (Some(value), _) => { - let channel = amino_unmarshal_binary_length_prefixed(&value)?; - - Ok(ChannelResponse::new( - query.port_id, - query.channel_id, - channel, - response.proof, - response.height.into(), - )) - } - (None, _) => Err(error::Kind::Rpc.context("Bad response").into()), - } + Ok(ChannelResponse::new( + query.port_id, + query.channel_id, + amino_unmarshal_binary_length_prefixed(&response.value)?, + response.proof, + response.height.into(), + )) } } diff --git a/relayer/cli/Cargo.toml b/relayer/cli/Cargo.toml index e101a695d1..a901fbc625 100644 --- a/relayer/cli/Cargo.toml +++ b/relayer/cli/Cargo.toml @@ -29,4 +29,4 @@ version = "0.5.2" [dev-dependencies] abscissa_core = { version = "0.5.2", features = ["testing"] } -once_cell = "1.2" +once_cell = "1.2" \ No newline at end of file diff --git a/relayer/cli/src/commands/query/channel.rs b/relayer/cli/src/commands/query/channel.rs index 918aece92f..2b3e6a96c7 100644 --- a/relayer/cli/src/commands/query/channel.rs +++ b/relayer/cli/src/commands/query/channel.rs @@ -8,9 +8,10 @@ use relayer_modules::ics24_host::identifier::{ChannelId, PortId}; use crate::commands::utils::block_on; use relayer::chain::tendermint::TendermintChain; +use relayer_modules::ics24_host::error::ValidationError; use tendermint::chain::Id as ChainId; -#[derive(Command, Debug, Options)] +#[derive(Clone, Command, Debug, Options)] pub struct QueryChannelEndsCmd { #[options(free, help = "identifier of the chain to query")] chain_id: Option, @@ -41,32 +42,42 @@ impl QueryChannelEndsCmd { &self, config: &Config, ) -> Result<(ChainConfig, QueryChannelOptions), String> { - match (&self.chain_id, &self.port_id, &self.channel_id) { - (Some(chain_id), Some(port_id), Some(channel_id)) => { - let chain_config = config.chains.iter().find(|c| c.id == *chain_id); - match chain_config { - Some(chain_config) => { - let opts = QueryChannelOptions { - port_id: port_id.parse().unwrap(), - channel_id: channel_id.parse().unwrap(), - height: match self.height { - Some(h) => h, - None => 0 as u64, - }, - proof: match self.proof { - Some(proof) => proof, - None => true, - }, - }; - Ok((chain_config.clone(), opts)) - } - _ => Err(format!("cannot find chain {} in config", chain_id)), - } - } - (None, _, _) => Err("missing chain identifier".to_string()), - (_, None, _) => Err("missing port identifier".to_string()), - (_, _, None) => Err("missing channel identifier".to_string()), - } + let chain_id = self + .chain_id + .ok_or_else(|| "missing chain identifier".to_string())?; + let chain_config = config + .chains + .iter() + .find(|c| c.id == chain_id) + .ok_or_else(|| "missing chain configuration".to_string())?; + + let port_id = self + .port_id + .as_ref() + .ok_or_else(|| "missing port identifier".to_string())? + .parse() + .map_err(|err: ValidationError| err.to_string())?; + + let channel_id = self + .channel_id + .as_ref() + .ok_or_else(|| "missing channel identifier".to_string())? + .parse() + .map_err(|err: ValidationError| err.to_string())?; + + let opts = QueryChannelOptions { + port_id, + channel_id, + height: match self.height { + Some(h) => h, + None => 0 as u64, + }, + proof: match self.proof { + Some(proof) => proof, + None => true, + }, + }; + Ok((chain_config.clone(), opts)) } } @@ -105,3 +116,112 @@ impl Runnable for QueryChannelEndsCmd { } } } + +#[cfg(test)] +mod tests { + use crate::commands::query::channel::QueryChannelEndsCmd; + use relayer::config::parse; + + #[test] + fn parse_query_ends_parameters() { + let default_params = QueryChannelEndsCmd { + chain_id: Some("ibc0".to_string().parse().unwrap()), + port_id: Some("transfer".to_string().parse().unwrap()), + channel_id: Some("testchannel".to_string().parse().unwrap()), + height: None, + proof: None, + }; + + struct Test { + name: String, + params: QueryChannelEndsCmd, + want_pass: bool, + } + + let tests: Vec = vec![ + Test { + name: "Good parameters".to_string(), + params: default_params.clone(), + want_pass: true, + }, + Test { + name: "No chain specified".to_string(), + params: QueryChannelEndsCmd { + chain_id: None, + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Chain not configured".to_string(), + params: QueryChannelEndsCmd { + chain_id: Some("notibc0oribc1".to_string().parse().unwrap()), + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "No port id specified".to_string(), + params: QueryChannelEndsCmd { + port_id: None, + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Bad port, non-alpha".to_string(), + params: QueryChannelEndsCmd { + port_id: Some("p34".to_string()), + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "No channel id specified".to_string(), + params: QueryChannelEndsCmd { + channel_id: None, + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Bad channel, name too short".to_string(), + params: QueryChannelEndsCmd { + channel_id: Some("chshort".to_string()), + ..default_params.clone() + }, + want_pass: false, + }, + ] + .into_iter() + .collect(); + + let path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/two_chains.toml" + ); + + let config = parse(path).unwrap(); + + for test in tests { + let res = test.params.validate_options(&config); + + match res { + Ok(_res) => { + assert!( + test.want_pass, + "validate_options should have failed for test {}", + test.name + ); + } + Err(err) => { + assert!( + !test.want_pass, + "validate_options failed for test {}, \nerr {}", + test.name, err + ); + } + } + } + } +} diff --git a/relayer/cli/src/commands/query/client.rs b/relayer/cli/src/commands/query/client.rs index 48b550636c..819e0763d2 100644 --- a/relayer/cli/src/commands/query/client.rs +++ b/relayer/cli/src/commands/query/client.rs @@ -8,9 +8,10 @@ use relayer_modules::ics24_host::identifier::ClientId; use crate::commands::utils::block_on; use relayer::chain::tendermint::TendermintChain; +use relayer_modules::ics24_host::error::ValidationError; use tendermint::chain::Id as ChainId; -#[derive(Command, Debug, Options)] +#[derive(Clone, Command, Debug, Options)] pub struct QueryClientStateCmd { #[options(free, help = "identifier of the chain to query")] chain_id: Option, @@ -39,8 +40,9 @@ impl QueryClientStateCmd { ) -> Result<(ChainConfig, QueryClientStateOptions), String> { let (chain_config, client_id) = validate_common_options(&self.chain_id, &self.client_id, config)?; + let opts = QueryClientStateOptions { - client_id: client_id.parse().unwrap(), + client_id, height: match self.height { Some(h) => h, None => 0 as u64, @@ -89,7 +91,7 @@ impl Runnable for QueryClientStateCmd { } } -#[derive(Command, Debug, Options)] +#[derive(Clone, Command, Debug, Options)] pub struct QueryClientConsensusCmd { #[options(free, help = "identifier of the chain to query")] chain_id: Option, @@ -126,7 +128,7 @@ impl QueryClientConsensusCmd { match self.consensus_height { Some(consensus_height) => { let opts = QueryClientConsensusOptions { - client_id: client_id.parse().unwrap(), + client_id, consensus_height, height: match self.height { Some(h) => h, @@ -188,28 +190,111 @@ fn validate_common_options( chain_id: &Option, client_id: &Option, config: &Config, -) -> Result<(ChainConfig, String), String> { - match (&chain_id, &client_id) { - (Some(chain_id), Some(client_id)) => { - let chain_config = config.chains.iter().find(|c| c.id == *chain_id); - - match chain_config { - Some(chain_config) => { - // check that the client_id is specified in one of the chain configurations - match config - .chains - .iter() - .find(|c| c.client_ids.contains(client_id)) - { - Some(_) => Ok((chain_config.clone(), client_id.parse().unwrap())), - None => Err(format!("cannot find client {} in config", client_id)), - } +) -> Result<(ChainConfig, ClientId), String> { + let chain_id = chain_id.ok_or_else(|| "missing chain parameter".to_string())?; + let chain_config = config + .chains + .iter() + .find(|c| c.id == chain_id) + .ok_or_else(|| "missing chain in configuration".to_string())?; + + let client_id = client_id + .as_ref() + .ok_or_else(|| "missing client identifier".to_string())? + .parse() + .map_err(|err: ValidationError| err.to_string())?; + + Ok((chain_config.clone(), client_id)) +} + +#[cfg(test)] +mod tests { + use crate::commands::query::client::QueryClientStateCmd; + use relayer::config::parse; + + #[test] + fn parse_query_state_parameters() { + let default_params = QueryClientStateCmd { + chain_id: Some("ibc0".to_string().parse().unwrap()), + client_id: Some("ibconeclient".to_string().parse().unwrap()), + height: None, + proof: None, + }; + + struct Test { + name: String, + params: QueryClientStateCmd, + want_pass: bool, + } + + let tests: Vec = vec![ + Test { + name: "Good parameters".to_string(), + params: default_params.clone(), + want_pass: true, + }, + Test { + name: "No chain specified".to_string(), + params: QueryClientStateCmd { + chain_id: None, + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Chain not configured".to_string(), + params: QueryClientStateCmd { + chain_id: Some("notibc0oribc1".to_string().parse().unwrap()), + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "No client id specified".to_string(), + params: QueryClientStateCmd { + client_id: None, + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Bad client id, non-alpha".to_string(), + params: QueryClientStateCmd { + client_id: Some("p34".to_string()), + ..default_params.clone() + }, + want_pass: false, + }, + ] + .into_iter() + .collect(); + + let path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/two_chains.toml" + ); + + let config = parse(path).unwrap(); + + for test in tests { + let res = test.params.validate_options(&config); + + match res { + Ok(_res) => { + assert!( + test.want_pass, + "validate_options should have failed for test {}", + test.name + ); + } + Err(err) => { + assert!( + !test.want_pass, + "validate_options failed for test {}, \nerr {}", + test.name, err + ); } - None => Err(format!("cannot find chain {} in config", chain_id)), } } - - (None, _) => Err("missing chain identifier".to_string()), - (_, None) => Err("missing client identifier".to_string()), } } diff --git a/relayer/cli/tests/fixtures/two_chains.toml b/relayer/cli/tests/fixtures/two_chains.toml new file mode 100644 index 0000000000..159af6923f --- /dev/null +++ b/relayer/cli/tests/fixtures/two_chains.toml @@ -0,0 +1,29 @@ +title = "IBC Relayer Config Example" + +[global] +timeout = "10s" +strategy = "naive" + +[[chains]] + id = "ibc0" + rpc_addr = "localhost:26657" + account_prefix = "cosmos" + key_name = "testkey" + store_prefix = "ibc" + client_ids = ["cla1", "cla2"] + gas = 200000 + gas_adjustement = 1.3 + gas_price = "0.025stake" + trusting_period = "336h" + +[[chains]] + id = "ibc0" + rpc_addr = "localhost:26557" + account_prefix = "cosmos" + key_name = "testkey" + store_prefix = "ibc" + client_ids = ["clb1"] + gas = 200000 + gas_adjustement = 1.3 + gas_price = "0.025stake" + trusting_period = "336h" \ No newline at end of file From b528e97b51ea2d51e186f062e283255c52a3f44e Mon Sep 17 00:00:00 2001 From: Shivani Joshi <46731446+Shivani912@users.noreply.github.com> Date: Tue, 26 May 2020 04:53:34 -0400 Subject: [PATCH 63/63] Connection queries and ConnOpenInit message (#77) * Add channel module and query * connection query implementation * Add MsgChanOpenInit message * Added more tests and error handling Use of 'map_err' to convert errors when parsing Return Resuls for constructor functions for channel and endpoint, and perform validation * add error kind and minor fixes * final fixes * impl validate_basic * impl MsgConnectionOpenInit * ConnectionEnd.versions changed to Vec * remove commented code * cargo fmt * impl issue#74 * attempt to fix clippy * review comments Co-authored-by: Anca Zamfir --- modules/src/ics03_connection/connection.rs | 109 ++++++++++ modules/src/ics03_connection/error.rs | 24 +++ modules/src/ics03_connection/exported.rs | 115 +++++++++++ modules/src/ics03_connection/mod.rs | 7 + modules/src/ics03_connection/msgs.rs | 181 ++++++++++++++++ modules/src/ics03_connection/query.rs | 110 ++++++++++ modules/src/ics04_channel/channel.rs | 32 +-- modules/src/ics04_channel/exported.rs | 10 +- modules/src/ics04_channel/msgs.rs | 8 +- modules/src/ics04_channel/query.rs | 6 +- modules/src/ics23_commitment/mod.rs | 5 + modules/src/lib.rs | 2 + modules/src/path.rs | 10 +- relayer/cli/src/commands/query.rs | 21 +- relayer/cli/src/commands/query/channel.rs | 30 +-- relayer/cli/src/commands/query/connection.rs | 204 +++++++++++++++++++ relayer/relay/src/query.rs | 1 + relayer/relay/src/query/connection.rs | 21 ++ 18 files changed, 849 insertions(+), 47 deletions(-) create mode 100644 modules/src/ics03_connection/connection.rs create mode 100644 modules/src/ics03_connection/error.rs create mode 100644 modules/src/ics03_connection/exported.rs create mode 100644 modules/src/ics03_connection/mod.rs create mode 100644 modules/src/ics03_connection/msgs.rs create mode 100644 modules/src/ics03_connection/query.rs create mode 100644 relayer/cli/src/commands/query/connection.rs create mode 100644 relayer/relay/src/query/connection.rs diff --git a/modules/src/ics03_connection/connection.rs b/modules/src/ics03_connection/connection.rs new file mode 100644 index 0000000000..66add54913 --- /dev/null +++ b/modules/src/ics03_connection/connection.rs @@ -0,0 +1,109 @@ +use super::exported::*; +use crate::ics03_connection::error; +use crate::ics03_connection::error::Kind; +use crate::ics23_commitment::CommitmentPrefix; +use crate::ics24_host::identifier::{ClientId, ConnectionId}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ConnectionEnd { + state: State, + client_id: ClientId, + counterparty: Counterparty, + versions: Vec, +} + +impl ConnectionEnd { + pub fn new(client_id: ClientId, counterparty: Counterparty, versions: Vec) -> Self { + ConnectionEnd { + state: State::Uninitialized, + client_id, + counterparty, + versions, + } + } +} + +impl Connection for ConnectionEnd { + type ValidationError = error::Error; + + fn state(&self) -> &State { + &self.state + } + + fn client_id(&self) -> String { + self.client_id.as_str().into() + } + + fn counterparty( + &self, + ) -> Box> { + Box::new(self.counterparty.clone()) + } + + fn versions(&self) -> Vec { + self.versions.clone() + } + + fn validate_basic(&self) -> Result<(), Self::ValidationError> { + if self.versions.is_empty() { + return Err(error::Kind::InvalidVersion + .context("missing connection versions") + .into()); + } + + for v in self.versions().iter() { + if v.trim().is_empty() { + return Err(error::Kind::InvalidVersion + .context("empty version string") + .into()); + } + } + self.counterparty().validate_basic() + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Counterparty { + client_id: ClientId, + connection_id: ConnectionId, + prefix: CommitmentPrefix, +} + +impl Counterparty { + pub fn new( + client_id: String, + connection_id: String, + prefix: CommitmentPrefix, + ) -> Result { + Ok(Self { + client_id: client_id + .parse() + .map_err(|e| Kind::IdentifierError.context(e))?, + connection_id: connection_id + .parse() + .map_err(|e| Kind::IdentifierError.context(e))?, + prefix, + }) + } +} + +impl ConnectionCounterparty for Counterparty { + type ValidationError = error::Error; + + fn client_id(&self) -> String { + self.client_id.as_str().into() + } + + fn connection_id(&self) -> String { + self.connection_id.as_str().into() + } + + fn prefix(&self) -> &CommitmentPrefix { + &self.prefix + } + + fn validate_basic(&self) -> Result<(), Self::ValidationError> { + todo!() + } +} diff --git a/modules/src/ics03_connection/error.rs b/modules/src/ics03_connection/error.rs new file mode 100644 index 0000000000..76b60ad117 --- /dev/null +++ b/modules/src/ics03_connection/error.rs @@ -0,0 +1,24 @@ +// TODO: Update error types for Connection!! + +use anomaly::{BoxError, Context}; +use thiserror::Error; + +pub type Error = anomaly::Error; + +#[derive(Clone, Debug, Error)] +pub enum Kind { + #[error("connection state unknown")] + UnknownState, + + #[error("identifier error")] + IdentifierError, + + #[error("invalid version")] + InvalidVersion, +} + +impl Kind { + pub fn context(self, source: impl Into) -> Context { + Context::new(self, Some(source.into())) + } +} diff --git a/modules/src/ics03_connection/exported.rs b/modules/src/ics03_connection/exported.rs new file mode 100644 index 0000000000..52eed14baf --- /dev/null +++ b/modules/src/ics03_connection/exported.rs @@ -0,0 +1,115 @@ +use super::error; +use crate::ics23_commitment::CommitmentPrefix; +use anomaly::fail; +use serde_derive::{Deserialize, Serialize}; + +pub trait Connection { + type ValidationError: std::error::Error; + + fn state(&self) -> &State; + fn client_id(&self) -> String; + fn counterparty( + &self, + ) -> Box>; + fn versions(&self) -> Vec; + fn validate_basic(&self) -> Result<(), Self::ValidationError>; +} + +pub trait ConnectionCounterparty { + type ValidationError: std::error::Error; + + fn client_id(&self) -> String; + fn connection_id(&self) -> String; + fn prefix(&self) -> &CommitmentPrefix; + fn validate_basic(&self) -> Result<(), Self::ValidationError>; +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum State { + Uninitialized = 0, + Init, + TryOpen, + Open, +} + +impl State { + /// Yields the state as a string + pub fn as_string(&self) -> &'static str { + match self { + Self::Uninitialized => "UNINITIALIZED", + Self::Init => "INIT", + Self::TryOpen => "TRYOPEN", + Self::Open => "OPEN", + } + } +} + +impl std::str::FromStr for State { + type Err = error::Error; + + fn from_str(s: &str) -> Result { + match s { + "UNINITIALIZED" => Ok(Self::Uninitialized), + "INIT" => Ok(Self::Init), + "TRYOPEN" => Ok(Self::TryOpen), + "OPEN" => Ok(Self::Open), + _ => fail!(error::Kind::UnknownState, s), + } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + #[test] + fn parse_connection_state() { + use super::State; + + struct Test { + state: &'static str, + want_res: State, + want_err: bool, + } + + let tests: Vec = vec![ + Test { + state: "UNINITIALIZED", + want_res: State::Uninitialized, + want_err: false, + }, + Test { + state: "INIT", + want_res: State::Init, + want_err: false, + }, + Test { + state: "TRYOPEN", + want_res: State::TryOpen, + want_err: false, + }, + Test { + state: "OPEN", + want_res: State::Open, + want_err: false, + }, + Test { + state: "INVALID_STATE", + want_res: State::Open, + want_err: true, + }, + ] + .into_iter() + .collect(); + + for test in tests { + match State::from_str(test.state) { + Ok(res) => { + assert!(!test.want_err); + assert_eq!(test.want_res, res); + } + Err(_) => assert!(test.want_err, "parse failed"), + } + } + } +} diff --git a/modules/src/ics03_connection/mod.rs b/modules/src/ics03_connection/mod.rs new file mode 100644 index 0000000000..a5a4405e9f --- /dev/null +++ b/modules/src/ics03_connection/mod.rs @@ -0,0 +1,7 @@ +//! ICS 03: IBC Connection implementation + +pub mod connection; +pub mod error; +pub mod exported; +pub mod msgs; +pub mod query; diff --git a/modules/src/ics03_connection/msgs.rs b/modules/src/ics03_connection/msgs.rs new file mode 100644 index 0000000000..ba1532c607 --- /dev/null +++ b/modules/src/ics03_connection/msgs.rs @@ -0,0 +1,181 @@ +use crate::ics03_connection::connection::Counterparty; +use crate::ics03_connection::error::Kind; +use crate::ics03_connection::exported::ConnectionCounterparty; +use crate::ics04_channel::msgs::Msg; +use crate::ics23_commitment::CommitmentPrefix; +use crate::ics24_host::identifier::{ClientId, ConnectionId}; +use crate::ics24_host::validate::{validate_client_identifier, validate_connection_identifier}; +use serde_derive::{Deserialize, Serialize}; +use tendermint::account::Id as AccountId; + +pub const TYPE_MSG_CONNECTION_OPEN_INIT: &str = "connection_open_init"; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct MsgConnectionOpenInit { + connection_id: ConnectionId, + client_id: ClientId, + counterparty: Counterparty, + signer: AccountId, +} + +impl MsgConnectionOpenInit { + pub fn new( + connection_id: String, + client_id: String, + counterparty_connection_id: String, + counterparty_client_id: String, + counterparty_commitment_prefix: CommitmentPrefix, + signer: AccountId, + ) -> Result { + Ok(Self { + connection_id: connection_id + .parse() + .map_err(|e| Kind::IdentifierError.context(e))?, + client_id: client_id + .parse() + .map_err(|e| Kind::IdentifierError.context(e))?, + counterparty: Counterparty::new( + counterparty_client_id, + counterparty_connection_id, + counterparty_commitment_prefix, + ) + .map_err(|e| Kind::IdentifierError.context(e))?, + signer, + }) + } +} + +impl Msg for MsgConnectionOpenInit { + type ValidationError = crate::ics03_connection::error::Error; + + fn route(&self) -> String { + crate::keys::ROUTER_KEY.to_string() + } + + fn get_type(&self) -> String { + TYPE_MSG_CONNECTION_OPEN_INIT.to_string() + } + + fn validate_basic(&self) -> Result<(), Self::ValidationError> { + validate_connection_identifier(self.connection_id.as_str()) + .map_err(|e| Kind::IdentifierError.context(e))?; + validate_client_identifier(self.client_id.as_str()) + .map_err(|e| Kind::IdentifierError.context(e))?; + self.counterparty + .validate_basic() + .map_err(|e| Kind::IdentifierError.context(e))?; + //todo: validate signer! + Ok(()) + } + + fn get_sign_bytes(&self) -> Vec { + todo!() + } + + fn get_signers(&self) -> Vec { + vec![self.signer] + } +} + +#[cfg(test)] +mod tests { + use crate::ics23_commitment::CommitmentPrefix; + use std::str::FromStr; + use tendermint::account::Id as AccountId; + + #[test] + fn parse_connection_open_init_msg() { + use super::MsgConnectionOpenInit; + let id_hex = "0CDA3F47EF3C4906693B170EF650EB968C5F4B2C"; + let acc = AccountId::from_str(id_hex).unwrap(); + + #[derive(Clone, Debug, PartialEq)] + struct ConOpenInitParams { + connection_id: String, + client_id: String, + counterparty_connection_id: String, + counterparty_client_id: String, + counterparty_commitment_prefix: CommitmentPrefix, + } + + let default_con_params = ConOpenInitParams { + connection_id: "srcconnection".to_string(), + client_id: "srcclient".to_string(), + counterparty_connection_id: "destconnection".to_string(), + counterparty_client_id: "destclient".to_string(), + counterparty_commitment_prefix: CommitmentPrefix {}, + }; + + struct Test { + name: String, + params: ConOpenInitParams, + want_pass: bool, + } + + let tests: Vec = vec![ + Test { + name: "Good parameters".to_string(), + params: default_con_params.clone(), + want_pass: true, + }, + Test { + name: "Bad connection id, non-alpha".to_string(), + params: ConOpenInitParams { + connection_id: "con007".to_string(), + ..default_con_params.clone() + }, + want_pass: false, + }, + Test { + name: "Bad client id, name too short".to_string(), + params: ConOpenInitParams { + client_id: "client".to_string(), + ..default_con_params.clone() + }, + want_pass: false, + }, + Test { + name: "Bad destination connection id, name too long".to_string(), + params: ConOpenInitParams { + counterparty_connection_id: "abcdefghijklmnopqrstu".to_string(), + ..default_con_params.clone() + }, + want_pass: false, + }, + ] + .into_iter() + .collect(); + + for test in tests { + let p = test.params.clone(); + + let msg = MsgConnectionOpenInit::new( + p.connection_id, + p.client_id, + p.counterparty_connection_id, + p.counterparty_client_id, + p.counterparty_commitment_prefix, + acc, + ); + + match msg { + Ok(_res) => { + assert!( + test.want_pass, + "MsgConnOpenInit::new should have failed for test {}, \nmsg {:?}", + test.name, + test.params.clone() + ); + } + Err(_err) => { + assert!( + !test.want_pass, + "MsgConnOpenInit::new failed for test {}, \nmsg {:?}", + test.name, + test.params.clone() + ); + } + } + } + } +} diff --git a/modules/src/ics03_connection/query.rs b/modules/src/ics03_connection/query.rs new file mode 100644 index 0000000000..1ab22d21a6 --- /dev/null +++ b/modules/src/ics03_connection/query.rs @@ -0,0 +1,110 @@ +use tendermint::rpc::endpoint::abci_query::AbciQuery; + +use tendermint::abci; + +use crate::ics23_commitment::{CommitmentPath, CommitmentProof}; +use crate::ics24_host::identifier::ConnectionId; + +use crate::error; +use crate::ics03_connection::connection::ConnectionEnd; +use crate::path::{ConnectionPath, Path}; +use crate::query::{IbcQuery, IbcResponse}; +use crate::Height; + +pub struct QueryConnection { + pub chain_height: Height, + pub connection_id: ConnectionId, + pub connection_path: ConnectionPath, + pub prove: bool, +} + +impl QueryConnection { + pub fn new(chain_height: Height, connection_id: ConnectionId, prove: bool) -> Self { + Self { + chain_height, + connection_id: connection_id.clone(), + connection_path: ConnectionPath::new(connection_id), + prove, + } + } +} + +impl IbcQuery for QueryConnection { + type Response = ConnectionResponse; + + fn path(&self) -> abci::Path { + "/store/ibc/key".parse().unwrap() + } + + fn height(&self) -> Height { + self.chain_height + } + + fn prove(&self) -> bool { + self.prove + } + + fn data(&self) -> Vec { + self.connection_path.to_key().into() + } +} + +pub struct ConnectionResponse { + pub connection: IdentifiedConnectionEnd, + pub proof: Option, + pub proof_path: CommitmentPath, + pub proof_height: Height, +} + +impl ConnectionResponse { + pub fn new( + connection_id: ConnectionId, + connection: ConnectionEnd, + abci_proof: Option, + proof_height: Height, + ) -> Self { + let proof_path = CommitmentPath::from_path(ConnectionPath::new(connection_id.clone())); + let identified_connection_end = IdentifiedConnectionEnd::new(connection, connection_id); + ConnectionResponse { + connection: identified_connection_end, + proof: abci_proof, + proof_path, + proof_height, + } + } +} + +impl IbcResponse for ConnectionResponse { + fn from_abci_response( + query: QueryConnection, + response: AbciQuery, + ) -> Result { + let connection = amino_unmarshal_binary_length_prefixed(&response.value)?; + + Ok(ConnectionResponse::new( + query.connection_id, + connection, + response.proof, + response.height.into(), + )) + } +} + +#[derive(Debug)] +pub struct IdentifiedConnectionEnd { + connection_end: ConnectionEnd, + connection_id: ConnectionId, +} + +impl IdentifiedConnectionEnd { + pub fn new(connection_end: ConnectionEnd, connection_id: ConnectionId) -> Self { + IdentifiedConnectionEnd { + connection_end, + connection_id, + } + } +} + +fn amino_unmarshal_binary_length_prefixed(_bytes: &[u8]) -> Result { + todo!() +} diff --git a/modules/src/ics04_channel/channel.rs b/modules/src/ics04_channel/channel.rs index f8dcbf1719..9c4bb86c7b 100644 --- a/modules/src/ics04_channel/channel.rs +++ b/modules/src/ics04_channel/channel.rs @@ -5,18 +5,18 @@ use crate::ics24_host::identifier::{ChannelId, ConnectionId, PortId}; use serde_derive::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Channel { +pub struct ChannelEnd { state: State, ordering: Order, - remote: Endpoint, + remote: Counterparty, connection_hops: Vec, version: String, } -impl Channel { +impl ChannelEnd { pub fn new( ordering: Order, - remote: Endpoint, + remote: Counterparty, connection_hops: Vec, version: String, ) -> Self { @@ -30,18 +30,20 @@ impl Channel { } } -impl ChannelI for Channel { +impl Channel for ChannelEnd { type ValidationError = crate::ics04_channel::error::Error; - fn state(&self) -> State { - self.state.clone() + fn state(&self) -> &State { + &self.state } - fn ordering(&self) -> Order { - self.ordering.clone() + fn ordering(&self) -> &Order { + &self.ordering } - fn counterparty(&self) -> Box> { + fn counterparty( + &self, + ) -> Box> { Box::new(self.remote.clone()) } @@ -69,12 +71,12 @@ impl ChannelI for Channel { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Endpoint { +pub struct Counterparty { port_id: PortId, channel_id: ChannelId, } -impl Endpoint { +impl Counterparty { pub fn new( port_id: String, channel_id: String, @@ -90,15 +92,15 @@ impl Endpoint { } } -impl CounterpartyI for Endpoint { +impl ChannelCounterparty for Counterparty { type ValidationError = crate::ics04_channel::error::Error; fn port_id(&self) -> String { - String::from(self.port_id.as_str()) + self.port_id.as_str().into() } fn channel_id(&self) -> String { - String::from(self.channel_id.as_str()) + self.channel_id.as_str().into() } fn validate_basic(&self) -> Result<(), Self::ValidationError> { diff --git a/modules/src/ics04_channel/exported.rs b/modules/src/ics04_channel/exported.rs index 17d962f288..86cd7e090b 100644 --- a/modules/src/ics04_channel/exported.rs +++ b/modules/src/ics04_channel/exported.rs @@ -3,17 +3,17 @@ use crate::ics24_host::identifier::ConnectionId; use anomaly::fail; use serde_derive::{Deserialize, Serialize}; -pub trait ChannelI { +pub trait Channel { type ValidationError: std::error::Error; - fn state(&self) -> State; - fn ordering(&self) -> Order; - fn counterparty(&self) -> Box>; + fn state(&self) -> &State; + fn ordering(&self) -> &Order; + fn counterparty(&self) -> Box>; fn connection_hops(&self) -> Vec; fn version(&self) -> String; fn validate_basic(&self) -> Result<(), Self::ValidationError>; } -pub trait CounterpartyI { +pub trait ChannelCounterparty { type ValidationError: std::error::Error; fn port_id(&self) -> String; fn channel_id(&self) -> String; diff --git a/modules/src/ics04_channel/msgs.rs b/modules/src/ics04_channel/msgs.rs index aafd8fcefb..eade4ab102 100644 --- a/modules/src/ics04_channel/msgs.rs +++ b/modules/src/ics04_channel/msgs.rs @@ -1,5 +1,5 @@ #![allow(clippy::too_many_arguments)] -use super::channel::{Channel, Endpoint}; +use super::channel::{ChannelEnd, Counterparty}; use super::exported::*; use crate::ics04_channel::error::Kind; use crate::ics24_host::identifier::{ChannelId, ConnectionId, PortId}; @@ -28,7 +28,7 @@ pub trait Msg { pub struct MsgChannelOpenInit { port_id: PortId, channel_id: ChannelId, - channel: Channel, + channel: ChannelEnd, signer: AccountId, } @@ -55,9 +55,9 @@ impl MsgChannelOpenInit { channel_id: channel_id .parse() .map_err(|e| Kind::IdentifierError.context(e))?, - channel: Channel::new( + channel: ChannelEnd::new( order.parse()?, - Endpoint::new(counterparty_port_id, counterparty_channel_id) + Counterparty::new(counterparty_port_id, counterparty_channel_id) .map_err(|e| Kind::IdentifierError.context(e))?, connection_hops.map_err(|e| Kind::IdentifierError.context(e))?, version, diff --git a/modules/src/ics04_channel/query.rs b/modules/src/ics04_channel/query.rs index f52ace5aa3..aa36036641 100644 --- a/modules/src/ics04_channel/query.rs +++ b/modules/src/ics04_channel/query.rs @@ -5,7 +5,7 @@ use tendermint::abci; use crate::ics23_commitment::{CommitmentPath, CommitmentProof}; use crate::error; -use crate::ics04_channel::channel::Channel; +use crate::ics04_channel::channel::ChannelEnd; use crate::ics24_host::identifier::{ChannelId, PortId}; use crate::path::{ChannelEndsPath, Path}; use crate::query::{IbcQuery, IbcResponse}; @@ -52,7 +52,7 @@ impl IbcQuery for QueryChannel { } pub struct ChannelResponse { - pub channel: Channel, + pub channel: ChannelEnd, pub proof: Option, pub proof_path: CommitmentPath, pub proof_height: Height, @@ -62,7 +62,7 @@ impl ChannelResponse { pub fn new( port_id: PortId, channel_id: ChannelId, - channel: Channel, + channel: ChannelEnd, abci_proof: Option, proof_height: Height, ) -> Self { diff --git a/modules/src/ics23_commitment/mod.rs b/modules/src/ics23_commitment/mod.rs index 92af1148a3..e22e962227 100644 --- a/modules/src/ics23_commitment/mod.rs +++ b/modules/src/ics23_commitment/mod.rs @@ -26,3 +26,8 @@ impl CommitmentProof { } } */ + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CommitmentPrefix; + +// TODO: impl CommitPrefix diff --git a/modules/src/lib.rs b/modules/src/lib.rs index a95ed85f2e..f24dde2a8b 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -14,12 +14,14 @@ //! Implementation of the following ICS modules: //! //! - ICS 02: Client +//! - ICS 03: Connection //! - ICS 07: Tendermint Client //! - ICS 23: Vector Commitment Scheme //! - ICS 24: Host Requirements pub mod error; pub mod ics02_client; +pub mod ics03_connection; pub mod ics04_channel; pub mod ics07_tendermint; pub mod ics23_commitment; diff --git a/modules/src/path.rs b/modules/src/path.rs index 7a6ec22460..ca9059de76 100644 --- a/modules/src/path.rs +++ b/modules/src/path.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::ics24_host::identifier::{ChannelId, ClientId, PortId}; +use crate::ics24_host::identifier::{ChannelId, ClientId, ConnectionId, PortId}; use crate::Height; mod cosmos; @@ -40,7 +40,13 @@ pub trait Path: Sized { } pub struct ConnectionPath { - pub connection_id: String, + pub connection_id: ConnectionId, +} + +impl ConnectionPath { + pub fn new(connection_id: ConnectionId) -> Self { + Self { connection_id } + } } impl Path for ConnectionPath { diff --git a/relayer/cli/src/commands/query.rs b/relayer/cli/src/commands/query.rs index 4d1c20e279..e601c60bdb 100644 --- a/relayer/cli/src/commands/query.rs +++ b/relayer/cli/src/commands/query.rs @@ -4,6 +4,7 @@ use abscissa_core::{Command, Options, Runnable}; mod channel; mod client; +mod connection; /// `query` subcommand #[derive(Command, Debug, Options, Runnable)] @@ -11,6 +12,11 @@ pub enum QueryCmd { /// The `query client` subcommand #[options(help = "query client")] Client(QueryClientCmds), + + /// The `query connection` subcommand + #[options(help = "query connection")] + Connection(QueryConnectionCmds), + /// The `query channel` subcommand #[options(help = "query channel")] Channel(QueryChannelCmds), @@ -21,14 +27,23 @@ pub enum QueryClientCmds { /// The `query client state` subcommand #[options(help = "query client full state")] State(client::QueryClientStateCmd), + /// The `query client consensus` subcommand + /// The `query client consensus` subcommand #[options(help = "query client consensus")] Consensus(client::QueryClientConsensusCmd), } +#[derive(Command, Debug, Options, Runnable)] +pub enum QueryConnectionCmds { + /// The `query connection end` subcommand + #[options(help = "query connection end")] + End(connection::QueryConnectionEndCmd), +} + #[derive(Command, Debug, Options, Runnable)] pub enum QueryChannelCmds { - /// The `query channel ends` subcommand - #[options(help = "query channel ends")] - Ends(channel::QueryChannelEndsCmd), + /// The `query channel end` subcommand + #[options(help = "query channel end")] + End(channel::QueryChannelEndCmd), } diff --git a/relayer/cli/src/commands/query/channel.rs b/relayer/cli/src/commands/query/channel.rs index 2b3e6a96c7..7f1cd460ac 100644 --- a/relayer/cli/src/commands/query/channel.rs +++ b/relayer/cli/src/commands/query/channel.rs @@ -12,7 +12,7 @@ use relayer_modules::ics24_host::error::ValidationError; use tendermint::chain::Id as ChainId; #[derive(Clone, Command, Debug, Options)] -pub struct QueryChannelEndsCmd { +pub struct QueryChannelEndCmd { #[options(free, help = "identifier of the chain to query")] chain_id: Option, @@ -37,7 +37,7 @@ struct QueryChannelOptions { proof: bool, } -impl QueryChannelEndsCmd { +impl QueryChannelEndCmd { fn validate_options( &self, config: &Config, @@ -81,7 +81,7 @@ impl QueryChannelEndsCmd { } } -impl Runnable for QueryChannelEndsCmd { +impl Runnable for QueryChannelEndCmd { fn run(&self) { let config = app_config(); @@ -95,10 +95,10 @@ impl Runnable for QueryChannelEndsCmd { status_info!("Options", "{:?}", opts); // run with proof: - // cargo run --bin relayer -- -c simple_config.toml query channel ends ibc0 transfer ibconexfer + // cargo run --bin relayer -- -c simple_config.toml query channel end ibc0 transfer ibconexfer // // run without proof: - // cargo run --bin relayer -- -c simple_config.toml query channel ends ibc0 transfer ibconexfer -p false + // cargo run --bin relayer -- -c simple_config.toml query channel end ibc0 transfer ibconexfer -p false // // Note: currently both fail in amino_unmarshal_binary_length_prefixed(). // To test this start a Gaia node and configure a channel using the go relayer. @@ -119,12 +119,12 @@ impl Runnable for QueryChannelEndsCmd { #[cfg(test)] mod tests { - use crate::commands::query::channel::QueryChannelEndsCmd; + use crate::commands::query::channel::QueryChannelEndCmd; use relayer::config::parse; #[test] - fn parse_query_ends_parameters() { - let default_params = QueryChannelEndsCmd { + fn parse_channel_query_end_parameters() { + let default_params = QueryChannelEndCmd { chain_id: Some("ibc0".to_string().parse().unwrap()), port_id: Some("transfer".to_string().parse().unwrap()), channel_id: Some("testchannel".to_string().parse().unwrap()), @@ -134,7 +134,7 @@ mod tests { struct Test { name: String, - params: QueryChannelEndsCmd, + params: QueryChannelEndCmd, want_pass: bool, } @@ -146,7 +146,7 @@ mod tests { }, Test { name: "No chain specified".to_string(), - params: QueryChannelEndsCmd { + params: QueryChannelEndCmd { chain_id: None, ..default_params.clone() }, @@ -154,7 +154,7 @@ mod tests { }, Test { name: "Chain not configured".to_string(), - params: QueryChannelEndsCmd { + params: QueryChannelEndCmd { chain_id: Some("notibc0oribc1".to_string().parse().unwrap()), ..default_params.clone() }, @@ -162,7 +162,7 @@ mod tests { }, Test { name: "No port id specified".to_string(), - params: QueryChannelEndsCmd { + params: QueryChannelEndCmd { port_id: None, ..default_params.clone() }, @@ -170,7 +170,7 @@ mod tests { }, Test { name: "Bad port, non-alpha".to_string(), - params: QueryChannelEndsCmd { + params: QueryChannelEndCmd { port_id: Some("p34".to_string()), ..default_params.clone() }, @@ -178,7 +178,7 @@ mod tests { }, Test { name: "No channel id specified".to_string(), - params: QueryChannelEndsCmd { + params: QueryChannelEndCmd { channel_id: None, ..default_params.clone() }, @@ -186,7 +186,7 @@ mod tests { }, Test { name: "Bad channel, name too short".to_string(), - params: QueryChannelEndsCmd { + params: QueryChannelEndCmd { channel_id: Some("chshort".to_string()), ..default_params.clone() }, diff --git a/relayer/cli/src/commands/query/connection.rs b/relayer/cli/src/commands/query/connection.rs new file mode 100644 index 0000000000..16c2f99cb4 --- /dev/null +++ b/relayer/cli/src/commands/query/connection.rs @@ -0,0 +1,204 @@ +use crate::prelude::*; + +use abscissa_core::{Command, Options, Runnable}; +use relayer::config::{ChainConfig, Config}; +use relayer::query::connection::query_connection; + +use crate::commands::utils::block_on; +use relayer::chain::tendermint::TendermintChain; +use relayer_modules::ics24_host::error::ValidationError; +use relayer_modules::ics24_host::identifier::ConnectionId; +use tendermint::chain::Id as ChainId; + +#[derive(Clone, Command, Debug, Options)] +pub struct QueryConnectionEndCmd { + #[options(free, help = "identifier of the chain to query")] + chain_id: Option, + + #[options(free, help = "identifier of the connection to query")] + connection_id: Option, + + #[options(help = "height of the state to query", short = "h")] + height: Option, + + #[options(help = "whether proof is required", short = "p")] + proof: Option, +} + +#[derive(Debug)] +struct QueryConnectionOptions { + connection_id: ConnectionId, + height: u64, + proof: bool, +} + +impl QueryConnectionEndCmd { + fn validate_options( + &self, + config: &Config, + ) -> Result<(ChainConfig, QueryConnectionOptions), String> { + let chain_id = self + .chain_id + .ok_or_else(|| "missing chain identifier".to_string())?; + let chain_config = config + .chains + .iter() + .find(|c| c.id == chain_id) + .ok_or_else(|| "missing chain configuration".to_string())?; + + let connection_id = self + .connection_id + .as_ref() + .ok_or_else(|| "missing connection identifier".to_string())? + .parse() + .map_err(|err: ValidationError| err.to_string())?; + + let opts = QueryConnectionOptions { + connection_id, + height: match self.height { + Some(h) => h, + None => 0 as u64, + }, + proof: match self.proof { + Some(proof) => proof, + None => true, + }, + }; + Ok((chain_config.clone(), opts)) + } +} + +impl Runnable for QueryConnectionEndCmd { + fn run(&self) { + let config = app_config(); + + let (chain_config, opts) = match self.validate_options(&config) { + Err(err) => { + status_err!("invalid options: {}", err); + return; + } + Ok(result) => result, + }; + status_info!("Options", "{:?}", opts); + + // run with proof: + // cargo run --bin relayer -- -c simple_config.toml query connection end ibc0 ibconeconnection + // + // run without proof: + // cargo run --bin relayer -- -c simple_config.toml query connection end ibc0 ibconeconnection -p false + // + // Note: currently both fail in amino_unmarshal_binary_length_prefixed(). + // To test this start a Gaia node and configure a client using the go relayer. + let chain = TendermintChain::from_config(chain_config).unwrap(); + let res = block_on(query_connection( + &chain, + opts.height, + opts.connection_id.clone(), + opts.proof, + )); + match res { + Ok(cs) => status_info!("connection query result: ", "{:?}", cs.connection), + Err(e) => status_info!("connection query error: ", "{:?}", e), + } + } +} + +#[cfg(test)] +mod tests { + use crate::commands::query::connection::QueryConnectionEndCmd; + use relayer::config::parse; + + #[test] + fn parse_connection_query_end_parameters() { + let default_params = QueryConnectionEndCmd { + chain_id: Some("ibc0".to_string().parse().unwrap()), + connection_id: Some("ibconeconnection".to_string().parse().unwrap()), + height: None, + proof: None, + }; + + struct Test { + name: String, + params: QueryConnectionEndCmd, + want_pass: bool, + } + + let tests: Vec = vec![ + Test { + name: "Good parameters".to_string(), + params: default_params.clone(), + want_pass: true, + }, + Test { + name: "No chain specified".to_string(), + params: QueryConnectionEndCmd { + chain_id: None, + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Chain not configured".to_string(), + params: QueryConnectionEndCmd { + chain_id: Some("notibc0oribc1".to_string().parse().unwrap()), + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "No connection id specified".to_string(), + params: QueryConnectionEndCmd { + connection_id: None, + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Bad connection, non-alpha".to_string(), + params: QueryConnectionEndCmd { + connection_id: Some("conn01".to_string()), + ..default_params.clone() + }, + want_pass: false, + }, + Test { + name: "Bad connection, name too short".to_string(), + params: QueryConnectionEndCmd { + connection_id: Some("connshort".to_string()), + ..default_params.clone() + }, + want_pass: false, + }, + ] + .into_iter() + .collect(); + + let path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/two_chains.toml" + ); + + let config = parse(path).unwrap(); + + for test in tests { + let res = test.params.validate_options(&config); + + match res { + Ok(_res) => { + assert!( + test.want_pass, + "validate_options should have failed for test {}", + test.name + ); + } + Err(err) => { + assert!( + !test.want_pass, + "validate_options failed for test {}, \nerr {}", + test.name, err + ); + } + } + } + } +} diff --git a/relayer/relay/src/query.rs b/relayer/relay/src/query.rs index 04132e01c1..c97f0880db 100644 --- a/relayer/relay/src/query.rs +++ b/relayer/relay/src/query.rs @@ -6,6 +6,7 @@ use relayer_modules::query::IbcQuery; pub mod channel; pub mod client; +pub mod connection; /// Perform an IBC `query` on the given `chain`, and return the corresponding IBC response. pub async fn ibc_query(chain: &C, query: Q) -> Result diff --git a/relayer/relay/src/query/connection.rs b/relayer/relay/src/query/connection.rs new file mode 100644 index 0000000000..1cacf5cf04 --- /dev/null +++ b/relayer/relay/src/query/connection.rs @@ -0,0 +1,21 @@ +use crate::chain::Chain; +use relayer_modules::Height; + +use super::ibc_query; +use relayer_modules::ics03_connection::query::{ConnectionResponse, QueryConnection}; +use relayer_modules::ics24_host::identifier::ConnectionId; + +use relayer_modules::error; + +pub async fn query_connection( + chain: &C, + chain_height: Height, + connection_id: ConnectionId, + prove: bool, +) -> Result +where + C: Chain, +{ + let query = QueryConnection::new(chain_height, connection_id, prove); + ibc_query(chain, query).await +}