From 96ee56d7b1c731c982f8085b9441e62cce974c00 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Fri, 20 Mar 2020 11:50:52 +0100 Subject: [PATCH] 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"]