diff --git a/.changelog/unreleased/breaking-changes/923-flex-error.md b/.changelog/unreleased/breaking-changes/923-flex-error.md new file mode 100644 index 000000000..78a0e2912 --- /dev/null +++ b/.changelog/unreleased/breaking-changes/923-flex-error.md @@ -0,0 +1,5 @@ +- All crates' error handling has been refactored to make use of + [`flex-error`](https://github.com/informalsystems/flex-error/). This gives + users greater flexibility in terms of the error handling/reporting systems + they want to use and is a critical step towards `no_std` support. + ([#923](https://github.com/informalsystems/tendermint-rs/pull/923)) diff --git a/abci/Cargo.toml b/abci/Cargo.toml index afc2176c3..cb6a70b16 100644 --- a/abci/Cargo.toml +++ b/abci/Cargo.toml @@ -21,18 +21,22 @@ path = "src/application/kvstore/main.rs" required-features = [ "binary", "kvstore-app" ] [features] +default = ["std", "eyre_tracer"] +eyre_tracer = ["flex-error/eyre_tracer"] client = [] echo-app = [] kvstore-app = [] binary = [ "structopt", "tracing-subscriber" ] +std = [ + "flex-error/std" +] [dependencies] bytes = "1.0" -eyre = "0.6" prost = "0.7" tendermint-proto = { version = "0.21.0", path = "../proto" } -thiserror = "1.0" tracing = "0.1" +flex-error = { version = "0.4.1", default-features = false } structopt = { version = "0.3", optional = true } tracing-subscriber = { version = "0.2", optional = true } diff --git a/abci/src/application/kvstore.rs b/abci/src/application/kvstore.rs index 6a1cf5097..4c174fca2 100644 --- a/abci/src/application/kvstore.rs +++ b/abci/src/application/kvstore.rs @@ -1,7 +1,7 @@ //! In-memory key/value store ABCI application. use crate::codec::{encode_varint, MAX_VARINT_LENGTH}; -use crate::{Application, Error, Result}; +use crate::{Application, Error}; use bytes::BytesMut; use std::collections::HashMap; use std::sync::mpsc::{channel, Receiver, Sender}; @@ -28,7 +28,7 @@ impl KeyValueStoreApp { } /// Attempt to retrieve the value associated with the given key. - pub fn get>(&self, key: K) -> Result<(i64, Option)> { + pub fn get>(&self, key: K) -> Result<(i64, Option), Error> { let (result_tx, result_rx) = channel(); channel_send( &self.cmd_tx, @@ -44,7 +44,7 @@ impl KeyValueStoreApp { /// /// Optionally returns any pre-existing value associated with the given /// key. - pub fn set(&self, key: K, value: V) -> Result> + pub fn set(&self, key: K, value: V) -> Result, Error> where K: AsRef, V: AsRef, @@ -202,12 +202,9 @@ impl KeyValueStoreDriver { } /// Run the driver in the current thread (blocking). - pub fn run(mut self) -> Result<()> { + pub fn run(mut self) -> Result<(), Error> { loop { - let cmd = self - .cmd_rx - .recv() - .map_err(|e| Error::ChannelRecv(e.to_string()))?; + let cmd = self.cmd_rx.recv().map_err(Error::channel_recv)?; match cmd { Command::GetInfo { result_tx } => { channel_send(&result_tx, (self.height, self.app_hash.clone()))? @@ -232,7 +229,7 @@ impl KeyValueStoreDriver { } } - fn commit(&mut self, result_tx: Sender<(i64, Vec)>) -> Result<()> { + fn commit(&mut self, result_tx: Sender<(i64, Vec)>) -> Result<(), Error> { // As in the Go-based key/value store, simply encode the number of // items as the "app hash" let mut app_hash = BytesMut::with_capacity(MAX_VARINT_LENGTH); @@ -263,12 +260,10 @@ enum Command { Commit { result_tx: Sender<(i64, Vec)> }, } -fn channel_send(tx: &Sender, value: T) -> Result<()> { - tx.send(value) - .map_err(|e| Error::ChannelSend(e.to_string()).into()) +fn channel_send(tx: &Sender, value: T) -> Result<(), Error> { + tx.send(value).map_err(Error::send) } -fn channel_recv(rx: &Receiver) -> Result { - rx.recv() - .map_err(|e| Error::ChannelRecv(e.to_string()).into()) +fn channel_recv(rx: &Receiver) -> Result { + rx.recv().map_err(Error::channel_recv) } diff --git a/abci/src/client.rs b/abci/src/client.rs index d8cf0db23..c5146f88a 100644 --- a/abci/src/client.rs +++ b/abci/src/client.rs @@ -1,7 +1,7 @@ //! Blocking ABCI client. use crate::codec::ClientCodec; -use crate::{Error, Result}; +use crate::Error; use std::net::{TcpStream, ToSocketAddrs}; use tendermint_proto::abci::{ request, response, RequestApplySnapshotChunk, RequestBeginBlock, RequestCheckTx, RequestCommit, @@ -31,8 +31,8 @@ impl ClientBuilder { /// Client constructor that attempts to connect to the given network /// address. - pub fn connect(self, addr: A) -> Result { - let stream = TcpStream::connect(addr)?; + pub fn connect(self, addr: A) -> Result { + let stream = TcpStream::connect(addr).map_err(Error::io)?; Ok(Client { codec: ClientCodec::new(stream, self.read_buf_size), }) @@ -56,73 +56,78 @@ macro_rules! perform { ($self:expr, $type:ident, $req:expr) => { match $self.perform(request::Value::$type($req))? { response::Value::$type(r) => Ok(r), - r => Err(Error::UnexpectedServerResponseType(stringify!($type).to_string(), r).into()), + r => { + Err(Error::unexpected_server_response_type(stringify!($type).to_string(), r).into()) + } } }; } impl Client { /// Ask the ABCI server to echo back a message. - pub fn echo(&mut self, req: RequestEcho) -> Result { + pub fn echo(&mut self, req: RequestEcho) -> Result { perform!(self, Echo, req) } /// Request information about the ABCI application. - pub fn info(&mut self, req: RequestInfo) -> Result { + pub fn info(&mut self, req: RequestInfo) -> Result { perform!(self, Info, req) } /// To be called once upon genesis. - pub fn init_chain(&mut self, req: RequestInitChain) -> Result { + pub fn init_chain(&mut self, req: RequestInitChain) -> Result { perform!(self, InitChain, req) } /// Query the application for data at the current or past height. - pub fn query(&mut self, req: RequestQuery) -> Result { + pub fn query(&mut self, req: RequestQuery) -> Result { perform!(self, Query, req) } /// Check the given transaction before putting it into the local mempool. - pub fn check_tx(&mut self, req: RequestCheckTx) -> Result { + pub fn check_tx(&mut self, req: RequestCheckTx) -> Result { perform!(self, CheckTx, req) } /// Signal the beginning of a new block, prior to any `DeliverTx` calls. - pub fn begin_block(&mut self, req: RequestBeginBlock) -> Result { + pub fn begin_block(&mut self, req: RequestBeginBlock) -> Result { perform!(self, BeginBlock, req) } /// Apply a transaction to the application's state. - pub fn deliver_tx(&mut self, req: RequestDeliverTx) -> Result { + pub fn deliver_tx(&mut self, req: RequestDeliverTx) -> Result { perform!(self, DeliverTx, req) } /// Signal the end of a block. - pub fn end_block(&mut self, req: RequestEndBlock) -> Result { + pub fn end_block(&mut self, req: RequestEndBlock) -> Result { perform!(self, EndBlock, req) } - pub fn flush(&mut self) -> Result { + pub fn flush(&mut self) -> Result { perform!(self, Flush, RequestFlush {}) } /// Commit the current state at the current height. - pub fn commit(&mut self) -> Result { + pub fn commit(&mut self) -> Result { perform!(self, Commit, RequestCommit {}) } /// Request that the application set an option to a particular value. - pub fn set_option(&mut self, req: RequestSetOption) -> Result { + pub fn set_option(&mut self, req: RequestSetOption) -> Result { perform!(self, SetOption, req) } /// Used during state sync to discover available snapshots on peers. - pub fn list_snapshots(&mut self) -> Result { + pub fn list_snapshots(&mut self) -> Result { perform!(self, ListSnapshots, RequestListSnapshots {}) } /// Called when bootstrapping the node using state sync. - pub fn offer_snapshot(&mut self, req: RequestOfferSnapshot) -> Result { + pub fn offer_snapshot( + &mut self, + req: RequestOfferSnapshot, + ) -> Result { perform!(self, OfferSnapshot, req) } @@ -130,7 +135,7 @@ impl Client { pub fn load_snapshot_chunk( &mut self, req: RequestLoadSnapshotChunk, - ) -> Result { + ) -> Result { perform!(self, LoadSnapshotChunk, req) } @@ -138,19 +143,16 @@ impl Client { pub fn apply_snapshot_chunk( &mut self, req: RequestApplySnapshotChunk, - ) -> Result { + ) -> Result { perform!(self, ApplySnapshotChunk, req) } - fn perform(&mut self, req: request::Value) -> Result { + fn perform(&mut self, req: request::Value) -> Result { self.codec.send(Request { value: Some(req) })?; let res = self .codec .next() - .ok_or(Error::ServerConnectionTerminated)??; - match res.value { - Some(value) => Ok(value), - None => Err(Error::MalformedServerResponse.into()), - } + .ok_or_else(Error::server_connection_terminated)??; + res.value.ok_or_else(Error::malformed_server_response) } } diff --git a/abci/src/codec.rs b/abci/src/codec.rs index 919e2e798..170d384e6 100644 --- a/abci/src/codec.rs +++ b/abci/src/codec.rs @@ -4,13 +4,14 @@ //! //! [tsp]: https://docs.tendermint.com/master/spec/abci/client-server.html#tsp -use crate::Result; use bytes::{Buf, BufMut, BytesMut}; use prost::Message; use std::io::{Read, Write}; use std::marker::PhantomData; use tendermint_proto::abci::{Request, Response}; +use crate::error::Error; + /// The maximum number of bytes we expect in a varint. We use this to check if /// we're encountering a decoding error for a varint. pub const MAX_VARINT_LENGTH: usize = 16; @@ -60,7 +61,7 @@ where S: Read, I: Message + Default, { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { loop { @@ -75,7 +76,7 @@ where // more let bytes_read = match self.stream.read(self.read_window.as_mut()) { Ok(br) => br, - Err(e) => return Some(Err(e.into())), + Err(e) => return Some(Err(Error::io(e))), }; if bytes_read == 0 { // The underlying stream terminated @@ -93,31 +94,38 @@ where O: Message, { /// Send a message using this codec. - pub fn send(&mut self, message: O) -> Result<()> { + pub fn send(&mut self, message: O) -> Result<(), Error> { encode_length_delimited(message, &mut self.write_buf)?; while !self.write_buf.is_empty() { - let bytes_written = self.stream.write(self.write_buf.as_ref())?; + let bytes_written = self + .stream + .write(self.write_buf.as_ref()) + .map_err(Error::io)?; + if bytes_written == 0 { - return Err(std::io::Error::new( + return Err(Error::io(std::io::Error::new( std::io::ErrorKind::WriteZero, "failed to write to underlying stream", - ) - .into()); + ))); } self.write_buf.advance(bytes_written); } - Ok(self.stream.flush()?) + + self.stream.flush().map_err(Error::io)?; + + Ok(()) } } /// Encode the given message with a length prefix. -pub fn encode_length_delimited(message: M, mut dst: &mut B) -> Result<()> +pub fn encode_length_delimited(message: M, mut dst: &mut B) -> Result<(), Error> where M: Message, B: BufMut, { let mut buf = BytesMut::new(); - message.encode(&mut buf)?; + message.encode(&mut buf).map_err(Error::encode)?; + let buf = buf.freeze(); encode_varint(buf.len() as u64, &mut dst); dst.put(buf); @@ -125,7 +133,7 @@ where } /// Attempt to decode a message of type `M` from the given source buffer. -pub fn decode_length_delimited(src: &mut BytesMut) -> Result> +pub fn decode_length_delimited(src: &mut BytesMut) -> Result, Error> where M: Message + Default, { @@ -148,7 +156,9 @@ where src.advance(delim_len + (encoded_len as usize)); let mut result_bytes = BytesMut::from(tmp.split_to(encoded_len as usize).as_ref()); - Ok(Some(M::decode(&mut result_bytes)?)) + let res = M::decode(&mut result_bytes).map_err(Error::decode)?; + + Ok(Some(res)) } } @@ -158,7 +168,7 @@ pub fn encode_varint(val: u64, mut buf: &mut B) { prost::encoding::encode_varint(val << 1, &mut buf); } -pub fn decode_varint(mut buf: &mut B) -> Result { - let len = prost::encoding::decode_varint(&mut buf)?; +pub fn decode_varint(mut buf: &mut B) -> Result { + let len = prost::encoding::decode_varint(&mut buf).map_err(Error::decode)?; Ok(len >> 1) } diff --git a/abci/src/error.rs b/abci/src/error.rs index a66021cd5..dcdbb7e17 100644 --- a/abci/src/error.rs +++ b/abci/src/error.rs @@ -1,22 +1,49 @@ //! tendermint-abci errors -use thiserror::Error; +use flex_error::{define_error, DisplayError}; +use tendermint_proto::abci::response::Value; -/// Errors that can be produced by tendermint-abci. -#[derive(Debug, Error)] -pub enum Error { - #[error("server connection terminated")] - ServerConnectionTerminated, +define_error! { + Error { + Io + [ DisplayError ] + | _ | { "I/O error" }, - #[error("malformed server response")] - MalformedServerResponse, + Encode + [ DisplayError ] + | _ | { "error encoding protocol buffer" }, - #[error("unexpected server response type: expected {0}, but got {1:?}")] - UnexpectedServerResponseType(String, tendermint_proto::abci::response::Value), + Decode + [ DisplayError ] + | _ | { "error encoding protocol buffer" }, - #[error("channel send error: {0}")] - ChannelSend(String), + ServerConnectionTerminated + | _ | { "server connection terminated" }, - #[error("channel receive error: {0}")] - ChannelRecv(String), + MalformedServerResponse + | _ | { "malformed server response" }, + + UnexpectedServerResponseType + { + expected: String, + got: Value, + } + | e | { + format_args!("unexpected server response type: expected {0}, but got {1:?}", + e.expected, e.got) + }, + + ChannelSend + | _ | { "channel send error" }, + + ChannelRecv + [ DisplayError ] + | _ | { "channel recv error" }, + } +} + +impl Error { + pub fn send(_e: std::sync::mpsc::SendError) -> Error { + Error::channel_send() + } } diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 356574372..2d6d602bb 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -57,12 +57,9 @@ mod application; #[cfg(feature = "client")] mod client; mod codec; -mod error; +pub mod error; mod server; -// Re-exported -pub use eyre::Result; - // Common exports pub use application::Application; #[cfg(feature = "client")] diff --git a/abci/src/server.rs b/abci/src/server.rs index b9e6dc51b..623143307 100644 --- a/abci/src/server.rs +++ b/abci/src/server.rs @@ -2,7 +2,7 @@ use crate::application::RequestDispatcher; use crate::codec::ServerCodec; -use crate::{Application, Result}; +use crate::{error::Error, Application}; use std::net::{TcpListener, TcpStream, ToSocketAddrs}; use std::thread; use tracing::{error, info}; @@ -31,13 +31,13 @@ impl ServerBuilder { /// Binds the server to the given address. You must subsequently call the /// [`Server::listen`] method in order for incoming connections' requests /// to be routed to the specified ABCI application. - pub fn bind(self, addr: Addr, app: App) -> Result> + pub fn bind(self, addr: Addr, app: App) -> Result, Error> where Addr: ToSocketAddrs, App: Application, { - let listener = TcpListener::bind(addr)?; - let local_addr = listener.local_addr()?.to_string(); + let listener = TcpListener::bind(addr).map_err(Error::io)?; + let local_addr = listener.local_addr().map_err(Error::io)?.to_string(); info!("ABCI server running at {}", local_addr); Ok(Server { app, @@ -71,9 +71,9 @@ pub struct Server { impl Server { /// Initiate a blocking listener for incoming connections. - pub fn listen(self) -> Result<()> { + pub fn listen(self) -> Result<(), Error> { loop { - let (stream, addr) = self.listener.accept()?; + let (stream, addr) = self.listener.accept().map_err(Error::io)?; let addr = addr.to_string(); info!("Incoming connection from: {}", addr); self.spawn_client_handler(stream, addr); diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml index d70b180b8..6c5c6b551 100644 --- a/light-client/Cargo.toml +++ b/light-client/Cargo.toml @@ -28,17 +28,20 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["rpc-client", "lightstore-sled"] +default = ["std", "eyre_tracer", "rpc-client", "lightstore-sled"] +eyre_tracer = ["flex-error/eyre_tracer"] rpc-client = ["tokio", "tendermint-rpc/http-client"] secp256k1 = ["tendermint/secp256k1", "tendermint-rpc/secp256k1"] lightstore-sled = ["sled"] unstable = [] +std = [ + "flex-error/std" +] [dependencies] tendermint = { version = "0.21.0", path = "../tendermint" } tendermint-rpc = { version = "0.21.0", path = "../rpc", default-features = false } -anomaly = { version = "0.2.0", features = ["serializer"] } contracts = "0.4.0" crossbeam-channel = "0.4.2" derive_more = "0.99.5" @@ -48,8 +51,8 @@ serde_cbor = "0.11.1" serde_derive = "1.0.106" sled = { version = "0.34.3", optional = true } static_assertions = "1.1.0" -thiserror = "1.0.15" tokio = { version = "1.0", features = ["rt"], optional = true } +flex-error = { version = "0.4.1", default-features = false } [dev-dependencies] tendermint-testgen = { path = "../testgen" } diff --git a/light-client/examples/light_client.rs b/light-client/examples/light_client.rs index 9c3b7ce36..f2c1a3824 100644 --- a/light-client/examples/light_client.rs +++ b/light-client/examples/light_client.rs @@ -3,7 +3,6 @@ use std::{ time::Duration, }; -use anomaly::BoxError; use gumdrop::Options; use tendermint::Hash; @@ -84,7 +83,7 @@ fn make_instance( addr: tendermint_rpc::Url, db_path: impl AsRef, opts: &SyncOpts, -) -> Result { +) -> Result> { let light_store = SledStore::open(db_path)?; let rpc_client = rpc::HttpClient::new(addr).unwrap(); let options = light_client::Options { @@ -105,7 +104,7 @@ fn make_instance( Ok(builder.build()) } -fn sync_cmd(opts: SyncOpts) -> Result<(), BoxError> { +fn sync_cmd(opts: SyncOpts) -> Result<(), Box> { let primary: PeerId = "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE".parse().unwrap(); let witness: PeerId = "CEFEEDBADFADAD0C0CEEFACADE0ADEADBEEFC0FF".parse().unwrap(); diff --git a/light-client/src/builder/error.rs b/light-client/src/builder/error.rs index a77a80885..66ee70fa9 100644 --- a/light-client/src/builder/error.rs +++ b/light-client/src/builder/error.rs @@ -1,58 +1,47 @@ //! Errors raised by the builder DSL -use anomaly::BoxError; -use anomaly::Context; +use flex_error::define_error; use tendermint::block::Height; use tendermint::Hash; -use thiserror::Error; use crate::components::io::IoError; +use crate::predicates::errors::VerificationError; + +define_error! { + Error { + Io + [ IoError ] + | _ | { "I/O error" }, + + HeightMismatch + { + given: Height, + found: Height, + } + | e | { + format_args!("height mismatch: given = {0}, found = {1}", + e.given, e.found) + }, + + HashMismatch + { + given: Hash, + found: Hash, + } + | e | { + format_args!("hash mismatch: given = {0}, found = {1}", + e.given, e.found) + }, + + InvalidLightBlock + [ VerificationError ] + | _ | { "invalid light block" }, + + NoTrustedStateInStore + | _ | { "no trusted state in store" }, + + EmptyWitnessList + | _ | { "empty witness list" }, -/// An error raised by the builder -pub type Error = anomaly::Error; - -/// The various error kinds raised by the builder -#[derive(Debug, Clone, Error, PartialEq)] -pub enum Kind { - /// I/O error - #[error("I/O error: {0}")] - Io(#[from] IoError), - - /// Height mismatch - #[error("height mismatch: given = {given}, found = {found}")] - HeightMismatch { - /// Height of trusted header - given: Height, - /// Height of fetched header - found: Height, - }, - - /// Hash mismatch - #[error("hash mismatch: given = {given}, found = {found}")] - HashMismatch { - /// Hash of trusted header - given: Hash, - /// hash of fetched header - found: Hash, - }, - - /// Invalid light block - #[error("invalid light block")] - InvalidLightBlock, - - /// No trusted state as found in the store - #[error("no trusted state in store")] - NoTrustedStateInStore, - - /// An empty witness list was given - #[error("empty witness list")] - EmptyWitnessList, -} - -impl Kind { - /// Add additional context (i.e. include a source error and capture a backtrace). - /// You can convert the resulting `Context` into an `Error` by calling `.into()`. - pub fn context(self, source: impl Into) -> Context { - Context::new(self, Some(source.into())) } } diff --git a/light-client/src/builder/light_client.rs b/light-client/src/builder/light_client.rs index eea8d5631..c02846d19 100644 --- a/light-client/src/builder/light_client.rs +++ b/light-client/src/builder/light_client.rs @@ -2,8 +2,7 @@ use tendermint::{block::Height, Hash}; -use crate::bail; -use crate::builder::error::{self, Error}; +use crate::builder::error::Error; use crate::components::clock::Clock; use crate::components::io::{AtHeight, Io}; use crate::components::scheduler::Scheduler; @@ -134,7 +133,7 @@ impl LightClientBuilder { let trusted_state = self .light_store .highest_trusted_or_verified() - .ok_or(error::Kind::NoTrustedStateInStore)?; + .ok_or_else(Error::no_trusted_state_in_store)?; self.trust_light_block(trusted_state) } @@ -148,22 +147,19 @@ impl LightClientBuilder { let trusted_state = self .io .fetch_light_block(AtHeight::At(trusted_height)) - .map_err(error::Kind::Io)?; + .map_err(Error::io)?; if trusted_state.height() != trusted_height { - bail!(error::Kind::HeightMismatch { - given: trusted_height, - found: trusted_state.height(), - }); + return Err(Error::height_mismatch( + trusted_height, + trusted_state.height(), + )); } let header_hash = self.hasher.hash_header(&trusted_state.signed_header.header); if header_hash != trusted_hash { - bail!(error::Kind::HashMismatch { - given: trusted_hash, - found: header_hash, - }); + return Err(Error::hash_mismatch(trusted_hash, header_hash)); } self.trust_light_block(trusted_state) @@ -175,19 +171,19 @@ impl LightClientBuilder { self.predicates .is_within_trust_period(header, self.options.trusting_period, now) - .map_err(|e| error::Kind::InvalidLightBlock.context(e))?; + .map_err(Error::invalid_light_block)?; self.predicates .is_header_from_past(header, self.options.clock_drift, now) - .map_err(|e| error::Kind::InvalidLightBlock.context(e))?; + .map_err(Error::invalid_light_block)?; self.predicates .validator_sets_match(light_block, &*self.hasher) - .map_err(|e| error::Kind::InvalidLightBlock.context(e))?; + .map_err(Error::invalid_light_block)?; self.predicates .next_validators_match(light_block, &*self.hasher) - .map_err(|e| error::Kind::InvalidLightBlock.context(e))?; + .map_err(Error::invalid_light_block)?; Ok(()) } diff --git a/light-client/src/builder/supervisor.rs b/light-client/src/builder/supervisor.rs index 0cb8ba647..7b7be6e4d 100644 --- a/light-client/src/builder/supervisor.rs +++ b/light-client/src/builder/supervisor.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::builder::error::{self, Error}; +use crate::builder::error::Error; use crate::peer_list::{PeerList, PeerListBuilder}; use crate::supervisor::Instance; use crate::types::PeerId; @@ -95,7 +95,7 @@ impl SupervisorBuilder { ) -> Result, Error> { let mut iter = witnesses.into_iter().peekable(); if iter.peek().is_none() { - return Err(error::Kind::EmptyWitnessList.into()); + return Err(Error::empty_witness_list()); } for (peer_id, address, instance) in iter { diff --git a/light-client/src/components/io.rs b/light-client/src/components/io.rs index e068edfa9..640583e38 100644 --- a/light-client/src/components/io.rs +++ b/light-client/src/components/io.rs @@ -1,8 +1,7 @@ //! Provides an interface and a default implementation of the `Io` component -use serde::{Deserialize, Serialize}; +use flex_error::{define_error, TraceError}; use std::time::Duration; -use thiserror::Error; #[cfg(feature = "rpc-client")] use tendermint_rpc::Client; @@ -11,6 +10,12 @@ use tendermint_rpc as rpc; use crate::types::{Height, LightBlock}; +#[cfg(feature = "tokio")] +type TimeoutError = flex_error::DisplayOnly; + +#[cfg(not(feature = "tokio"))] +type TimeoutError = flex_error::NoSource; + /// Type for selecting either a specific height or the latest one pub enum AtHeight { /// A specific height @@ -29,34 +34,44 @@ impl From for AtHeight { } } -/// I/O errors -#[derive(Clone, Debug, Error, PartialEq, Serialize, Deserialize)] -pub enum IoError { - /// Wrapper for a `tendermint::rpc::Error`. - #[error(transparent)] - RpcError(#[from] rpc::Error), - - /// Given height is invalid - #[error("invalid height: {0}")] - InvalidHeight(String), - - /// Fetched validator set is invalid - #[error("fetched validator set is invalid: {0}")] - InvalidValidatorSet(String), +define_error! { + #[derive(Debug)] + IoError { + Rpc + [ rpc::Error ] + | _ | { "rpc error" }, + + InvalidHeight + | _ | { + "invalid height: given height must be greater than 0" + }, + + InvalidValidatorSet + [ tendermint::Error ] + | _ | { "fetched validator set is invalid" }, + + Timeout + { duration: Duration } + [ TimeoutError ] + | e | { + format_args!("task timed out after {} ms", + e.duration.as_millis()) + }, + + Runtime + [ TraceError ] + | _ | { "failed to initialize runtime" }, - /// Task timed out. - #[error("task timed out after {} ms", .0.as_millis())] - Timeout(Duration), - - /// Failed to initialize runtime - #[error("failed to initialize runtime")] - Runtime, + } } -impl IoError { +impl IoErrorDetail { /// Whether this error means that a timeout occured when querying a node. - pub fn is_timeout(&self) -> bool { - matches!(self, Self::Timeout(_)) + pub fn is_timeout(&self) -> Option { + match self { + Self::Timeout(e) => Some(e.duration), + _ => None, + } } } @@ -84,7 +99,6 @@ mod prod { use std::time::Duration; - use crate::bail; use crate::types::PeerId; use crate::utils::block_on; @@ -150,7 +164,7 @@ mod prod { match res { Ok(response) => Ok(response.signed_header), - Err(err) => Err(IoError::RpcError(err)), + Err(err) => Err(IoError::rpc(err)), } } @@ -160,9 +174,9 @@ mod prod { proposer_address: Option, ) -> Result { let height = match height { - AtHeight::Highest => bail!(IoError::InvalidHeight( - "given height must be greater than 0".to_string() - )), + AtHeight::Highest => { + return Err(IoError::invalid_height()); + } AtHeight::At(height) => height, }; @@ -170,12 +184,12 @@ mod prod { let response = block_on(self.timeout, async move { client.validators(height, Paging::All).await })? - .map_err(IoError::RpcError)?; + .map_err(IoError::rpc)?; let validator_set = match proposer_address { Some(proposer_address) => { TMValidatorSet::with_proposer(response.validators, proposer_address) - .map_err(|e| IoError::InvalidValidatorSet(e.to_string()))? + .map_err(IoError::invalid_validator_set)? } None => TMValidatorSet::without_proposer(response.validators), }; diff --git a/light-client/src/components/verifier.rs b/light-client/src/components/verifier.rs index 21249ddf5..57a6e40cf 100644 --- a/light-client/src/components/verifier.rs +++ b/light-client/src/components/verifier.rs @@ -1,5 +1,6 @@ //! Provides an interface and default implementation of the `Verifier` component +use crate::operations::voting_power::VotingPowerTally; use crate::predicates as preds; use crate::{ errors::ErrorExt, @@ -10,7 +11,10 @@ use crate::{ }, types::{LightBlock, Time}, }; -use preds::{errors::VerificationError, ProdPredicates, VerificationPredicates}; +use preds::{ + errors::{VerificationError, VerificationErrorDetail}, + ProdPredicates, VerificationPredicates, +}; use serde::{Deserialize, Serialize}; /// Represents the result of the verification performed by the @@ -21,17 +25,19 @@ pub enum Verdict { Success, /// The minimum voting power threshold is not reached, /// the block cannot be trusted yet. - NotEnoughTrust(VerificationError), + NotEnoughTrust(VotingPowerTally), /// Verification failed, the block is invalid. - Invalid(VerificationError), + Invalid(VerificationErrorDetail), } impl From> for Verdict { fn from(result: Result<(), VerificationError>) -> Self { match result { Ok(()) => Self::Success, - Err(e) if e.not_enough_trust() => Self::NotEnoughTrust(e), - Err(e) => Self::Invalid(e), + Err(VerificationError(e, _)) => match e.not_enough_trust() { + Some(tally) => Self::NotEnoughTrust(tally), + _ => Self::Invalid(e), + }, } } } diff --git a/light-client/src/errors.rs b/light-client/src/errors.rs index 00c69d78c..c92286384 100644 --- a/light-client/src/errors.rs +++ b/light-client/src/errors.rs @@ -1,106 +1,119 @@ //! Toplevel errors raised by the light client. use std::fmt::Debug; +use std::time::Duration; -use anomaly::{BoxError, Context}; +use crate::operations::voting_power::VotingPowerTally; use crossbeam_channel as crossbeam; -use serde::{Deserialize, Serialize}; -use thiserror::Error; use crate::{ components::io::IoError, light_client::Options, - predicates::errors::VerificationError, + predicates::errors::VerificationErrorDetail, types::{Hash, Height, LightBlock, PeerId, Status}, }; +use flex_error::{define_error, DisplayError, TraceError}; + +#[cfg(feature = "sled")] +type SledError = TraceError; + +#[cfg(not(feature = "sled"))] +type SledError = flex_error::NoSource; + +define_error! { + #[derive(Debug)] + Error { + Io + [ IoError ] + | _ | { "io error" }, + + NoPrimary + | _ | { "no primary" }, + + NoWitnesses + | _ | { "no witnesses" }, + + NoWitnessesLeft + | _ | { "no witnesses left" }, + + ForkDetected + { peers: Vec } + | e | { + format_args!("fork detected peers={0:?}", + e.peers) + }, + + NoInitialTrustedState + | _ | { "no initial trusted state" }, + + NoTrustedState + { status: Status } + | e | { + format_args!("no trusted state with status {:?}", + e.status) + }, + + TargetLowerThanTrustedState + { + target_height: Height, + trusted_height: Height, + } + | e | { + format_args!("target height ({0}) is lower than trusted state ({1})", + e.target_height, e.trusted_height) + }, + + TrustedStateOutsideTrustingPeriod + { + trusted_state: Box, + options: Options, + } + | _ | { + format_args!("trusted state outside of trusting period") + }, + + BisectionFailed + { + target_height: Height, + trusted_height: Height + } + | e | { + format_args!("bisection for target at height {0} failed when reached trusted state at height {1}", + e.target_height, e.trusted_height) + }, + + InvalidLightBlock + [ DisplayError ] + | _ | { "invalid light block" }, + + InvalidAdjacentHeaders + { + hash1: Hash, + hash2: Hash, + } + | e | { + format_args!("hash mismatch between two adjacent headers: {0} != {1}", + e.hash1, e.hash2) + }, + + MissingLastBlockId + { height: Height } + | e | { + format_args!("missing last_block_id for header at height {0}", + e.height) + }, + + ChannelDisconnected + | _ | { "internal channel disconnected" }, + + Sled + [ SledError ] + | _ | { "sled error" }, + + SerdeCbor + [ TraceError ] + | _ | { "serde cbor error" }, -/// An error raised by this library -pub type Error = anomaly::Error; - -/// The various error kinds raised by this library -#[derive(Debug, Clone, Error, PartialEq, Serialize, Deserialize)] -pub enum ErrorKind { - /// I/O error - #[error("I/O error: {0}")] - Io(#[from] IoError), - - /// Store error - #[error("store error")] - Store, - - /// No primary - #[error("no primary")] - NoPrimary, - - /// No witnesses - #[error("no witnesses")] - NoWitnesses, - - /// No witness left - #[error("no witness left")] - NoWitnessLeft, - - /// A fork has been detected between some peers - #[error("fork detected peers={0:?}")] - ForkDetected(Vec), - - /// No initial trusted state - #[error("no initial trusted state")] - NoInitialTrustedState, - - /// No trusted state - #[error("no trusted state")] - NoTrustedState(Status), - - /// Target height for the light client lower than latest trusted state height - #[error("target height ({target_height}) is lower than trusted state ({trusted_height})")] - TargetLowerThanTrustedState { - /// Target height - target_height: Height, - /// Latest trusted state height - trusted_height: Height, - }, - - /// The trusted state is outside of the trusting period - #[error("trusted state outside of trusting period")] - TrustedStateOutsideTrustingPeriod { - /// Trusted state - trusted_state: Box, - /// Light client options - options: Options, - }, - - /// Bisection failed when reached trusted state - #[error("bisection for target at height {0} failed when reached trusted state at height {1}")] - BisectionFailed(Height, Height), - - /// Verification failed for a light block - #[error("invalid light block: {0}")] - InvalidLightBlock(#[source] VerificationError), - - /// Hash mismatch between two adjacent headers - #[error("hash mismatch between two adjacent headers: {h1} != {h2}")] - InvalidAdjacentHeaders { - /// Hash #1 - h1: Hash, - /// Hash #2 - h2: Hash, - }, - - /// Missing last_block_id field for header at given height - #[error("missing last_block_id for header at height {0}")] - MissingLastBlockId(Height), - - /// Internal channel disconnected - #[error("internal channel disconnected")] - ChannelDisconnected, -} - -impl ErrorKind { - /// Add additional context (i.e. include a source error and capture a backtrace). - /// You can convert the resulting `Context` into an `Error` by calling `.into()`. - pub fn context(self, source: impl Into) -> Context { - Context::new(self, Some(source.into())) } } @@ -108,7 +121,7 @@ impl ErrorKind { pub trait ErrorExt { /// Whether this error means that the light block /// cannot be trusted w.r.t. the latest trusted state. - fn not_enough_trust(&self) -> bool; + fn not_enough_trust(&self) -> Option; /// Whether this error means that the light block has expired, /// ie. it's outside of the trusting period. @@ -116,44 +129,42 @@ pub trait ErrorExt { /// Whether this error means that a timeout occured when /// querying a node. - fn is_timeout(&self) -> bool; + fn is_timeout(&self) -> Option; } -impl ErrorExt for ErrorKind { - fn not_enough_trust(&self) -> bool { +impl ErrorExt for ErrorDetail { + fn not_enough_trust(&self) -> Option { if let Self::InvalidLightBlock(e) = self { - e.not_enough_trust() + e.source.not_enough_trust() } else { - false + None } } fn has_expired(&self) -> bool { if let Self::InvalidLightBlock(e) = self { - e.has_expired() + e.source.has_expired() } else { false } } /// Whether this error means that a timeout occured when querying a node. - fn is_timeout(&self) -> bool { + fn is_timeout(&self) -> Option { if let Self::Io(e) = self { - e.is_timeout() + e.source.is_timeout() } else { - false + None } } } -impl From> for ErrorKind { - fn from(_err: crossbeam::SendError) -> Self { - Self::ChannelDisconnected +impl Error { + pub fn send(_e: crossbeam::SendError) -> Error { + Error::channel_disconnected() } -} -impl From for ErrorKind { - fn from(_err: crossbeam::RecvError) -> Self { - Self::ChannelDisconnected + pub fn recv(_e: crossbeam::RecvError) -> Error { + Error::channel_disconnected() } } diff --git a/light-client/src/evidence.rs b/light-client/src/evidence.rs index 83c02108a..279fb9501 100644 --- a/light-client/src/evidence.rs +++ b/light-client/src/evidence.rs @@ -44,15 +44,13 @@ mod prod { fn report(&self, e: Evidence, peer: PeerId) -> Result { let client = self.rpc_client_for(peer)?; - let res = block_on( + let response = block_on( self.timeout, async move { client.broadcast_evidence(e).await }, - )?; + )? + .map_err(IoError::rpc)?; - match res { - Ok(response) => Ok(response.hash), - Err(err) => Err(IoError::RpcError(err)), - } + Ok(response.hash) } } @@ -70,7 +68,7 @@ mod prod { #[pre(self.peer_map.contains_key(&peer))] fn rpc_client_for(&self, peer: PeerId) -> Result { let peer_addr = self.peer_map.get(&peer).unwrap().to_owned(); - rpc::HttpClient::new(peer_addr).map_err(IoError::from) + rpc::HttpClient::new(peer_addr).map_err(IoError::rpc) } } } diff --git a/light-client/src/fork_detector.rs b/light-client/src/fork_detector.rs index a626470b3..339592694 100644 --- a/light-client/src/fork_detector.rs +++ b/light-client/src/fork_detector.rs @@ -1,9 +1,7 @@ //! Fork detection data structures and implementation. -use serde::{Deserialize, Serialize}; - use crate::{ - errors::{Error, ErrorExt, ErrorKind}, + errors::{Error, ErrorDetail, ErrorExt}, operations::{Hasher, ProdHasher}, state::State, store::memory::MemoryStore, @@ -12,7 +10,7 @@ use crate::{ }; /// Result of fork detection -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug)] pub enum ForkDetection { /// One or more forks have been detected Detected(Vec), @@ -21,7 +19,7 @@ pub enum ForkDetection { } /// Types of fork -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug)] pub enum Fork { /// An actual fork was found for this `LightBlock` Forked { @@ -31,9 +29,9 @@ pub enum Fork { witness: LightBlock, }, /// The node has been deemed faulty for this `LightBlock` - Faulty(LightBlock, ErrorKind), + Faulty(LightBlock, ErrorDetail), /// The node has timed out - Timeout(PeerId, ErrorKind), + Timeout(PeerId, ErrorDetail), } /// Interface for a fork detector @@ -122,16 +120,19 @@ impl ForkDetector for ProdForkDetector { primary: verified_block.clone(), witness: witness_block, }), - Err(e) if e.kind().has_expired() => { + Err(Error(e, _)) if e.has_expired() => { forks.push(Fork::Forked { primary: verified_block.clone(), witness: witness_block, }); } - Err(e) if e.kind().is_timeout() => { - forks.push(Fork::Timeout(witness_block.provider, e.kind().clone())) + Err(Error(e, _)) => { + if e.is_timeout().is_some() { + forks.push(Fork::Timeout(witness_block.provider, e)) + } else { + forks.push(Fork::Faulty(witness_block, e)) + } } - Err(e) => forks.push(Fork::Faulty(witness_block, e.kind().clone())), } } diff --git a/light-client/src/lib.rs b/light-client/src/lib.rs index ec0b53191..630d6d79f 100644 --- a/light-client/src/lib.rs +++ b/light-client/src/lib.rs @@ -1,7 +1,6 @@ #![forbid(unsafe_code)] #![deny( warnings, - missing_docs, trivial_casts, trivial_numeric_casts, unused_import_braces, @@ -34,8 +33,5 @@ pub mod types; pub(crate) mod utils; -#[doc(hidden)] -mod macros; - #[doc(hidden)] pub mod tests; diff --git a/light-client/src/light_client.rs b/light-client/src/light_client.rs index ac8565035..f5a06bd5f 100644 --- a/light-client/src/light_client.rs +++ b/light-client/src/light_client.rs @@ -9,10 +9,9 @@ use derive_more::Display; use serde::{Deserialize, Serialize}; use crate::{ - bail, components::{clock::Clock, io::*, scheduler::*, verifier::*}, contracts::*, - errors::{Error, ErrorKind}, + errors::Error, operations::Hasher, state::State, types::{Height, LightBlock, PeerId, Status, TrustThreshold}, @@ -120,10 +119,10 @@ impl LightClient { /// /// Note: This function delegates the actual work to `verify_to_target`. pub fn verify_to_highest(&mut self, state: &mut State) -> Result { - let target_block = match self.io.fetch_light_block(AtHeight::Highest) { - Ok(last_block) => last_block, - Err(io_error) => bail!(ErrorKind::Io(io_error)), - }; + let target_block = self + .io + .fetch_light_block(AtHeight::Highest) + .map_err(Error::io)?; self.verify_to_target(target_block.height(), state) } @@ -138,8 +137,8 @@ impl LightClient { /// should be trusted based on a previously verified light block. /// - When doing _forward_ verification, the Scheduler component decides which height to try to /// verify next, in case the current block pass verification but cannot be trusted yet. - /// - When doing _backward_ verification, the Hasher component is used to determine - /// whether the `last_block_id` hash of a block matches the hash of the block right below it. + /// - When doing _backward_ verification, the Hasher component is used to determine whether the + /// `last_block_id` hash of a block matches the hash of the block right below it. /// /// ## Implements /// - [LCV-DIST-SAFE.1] @@ -179,7 +178,7 @@ impl LightClient { let highest = state .light_store .highest_trusted_or_verified() - .ok_or(ErrorKind::NoInitialTrustedState)?; + .ok_or_else(Error::no_initial_trusted_state)?; if target_height >= highest.height() { // Perform forward verification with bisection @@ -205,21 +204,21 @@ impl LightClient { let trusted_state = state .light_store .highest_trusted_or_verified() - .ok_or(ErrorKind::NoInitialTrustedState)?; + .ok_or_else(Error::no_initial_trusted_state)?; if target_height < trusted_state.height() { - bail!(ErrorKind::TargetLowerThanTrustedState { + return Err(Error::target_lower_than_trusted_state( target_height, - trusted_height: trusted_state.height() - }); + trusted_state.height(), + )); } // Check invariant [LCV-INV-TP.1] if !is_within_trust_period(&trusted_state, self.options.trusting_period, now) { - bail!(ErrorKind::TrustedStateOutsideTrustingPeriod { - trusted_state: Box::new(trusted_state), - options: self.options, - }); + return Err(Error::trusted_state_outside_trusting_period( + Box::new(trusted_state), + self.options, + )); } // Log the current height as a dependency of the block at the target height @@ -252,7 +251,7 @@ impl LightClient { // and abort. state.light_store.update(¤t_block, Status::Failed); - bail!(ErrorKind::InvalidLightBlock(e)) + return Err(Error::invalid_light_block(e)); } Verdict::NotEnoughTrust(_) => { // The current block cannot be trusted because of a missing overlap in the @@ -282,13 +281,12 @@ impl LightClient { let trusted_state = state .light_store .highest_trusted_or_verified() - .ok_or(ErrorKind::NoInitialTrustedState)?; + .ok_or_else(Error::no_initial_trusted_state)?; - Err(ErrorKind::TargetLowerThanTrustedState { + Err(Error::target_lower_than_trusted_state( target_height, - trusted_height: trusted_state.height(), - } - .into()) + trusted_state.height(), + )) } /// Perform sequential backward verification. @@ -320,16 +318,16 @@ impl LightClient { let root = state .light_store .highest_trusted_or_verified() - .ok_or(ErrorKind::NoInitialTrustedState)?; + .ok_or_else(Error::no_initial_trusted_state)?; assert!(root.height() >= target_height); // Check invariant [LCV-INV-TP.1] if !is_within_trust_period(&root, self.options.trusting_period, self.clock.now()) { - bail!(ErrorKind::TrustedStateOutsideTrustingPeriod { - trusted_state: Box::new(root), - options: self.options, - }); + return Err(Error::trusted_state_outside_trusting_period( + Box::new(root), + self.options, + )); } // Compute a range of `Height`s from `trusted_height - 1` to `target_height`, inclusive. @@ -345,15 +343,15 @@ impl LightClient { .signed_header .header .last_block_id - .ok_or_else(|| ErrorKind::MissingLastBlockId(latest.height()))?; + .ok_or_else(|| Error::missing_last_block_id(latest.height()))?; let current_hash = self.hasher.hash_header(¤t.signed_header.header); if current_hash != latest_last_block_id.hash { - bail!(ErrorKind::InvalidAdjacentHeaders { - h1: current_hash, - h2: latest_last_block_id.hash - }); + return Err(Error::invalid_adjacent_headers( + current_hash, + latest_last_block_id.hash, + )); } // `latest` and `current` are linked together by `last_block_id`, @@ -396,7 +394,7 @@ impl LightClient { let block = self .io .fetch_light_block(AtHeight::At(height)) - .map_err(ErrorKind::Io)?; + .map_err(Error::io)?; state.light_store.insert(block.clone(), Status::Unverified); diff --git a/light-client/src/macros.rs b/light-client/src/macros.rs deleted file mode 100644 index 284ebe651..000000000 --- a/light-client/src/macros.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Small macros used internally. - -/// Bail out of the current function with the given error kind. -#[macro_export] -macro_rules! bail { - ($kind:expr) => { - return Err($kind.into()) - }; -} - -/// Ensure a condition holds, returning an error if it doesn't (ala `assert`). -#[macro_export] -macro_rules! ensure { - ($cond:expr, $kind:expr) => { - if !($cond) { - return Err($kind.into()); - } - }; -} diff --git a/light-client/src/operations/commit_validator.rs b/light-client/src/operations/commit_validator.rs index b5a0f8158..a779e4736 100644 --- a/light-client/src/operations/commit_validator.rs +++ b/light-client/src/operations/commit_validator.rs @@ -1,7 +1,6 @@ //! Provides an interface and default implementation for the `CommitValidator` operation use crate::{ - bail, operations::{Hasher, ProdHasher}, predicates::errors::VerificationError, types::{SignedHeader, ValidatorSet}, @@ -59,18 +58,15 @@ impl CommitValidator for ProdCommitValidator { // See https://github.com/informalsystems/tendermint-rs/issues/650 let has_present_signatures = signatures.iter().any(|cs| !cs.is_absent()); if !has_present_signatures { - bail!(VerificationError::ImplementationSpecific( - "no signatures for commit".to_string() - )); + return Err(VerificationError::no_signature_for_commit()); } // Check that that the number of signatures matches the number of validators. if signatures.len() != validator_set.validators().len() { - bail!(VerificationError::ImplementationSpecific(format!( - "pre-commit length: {} doesn't match validator length: {}", + return Err(VerificationError::mismatch_pre_commit_length( signatures.len(), - validator_set.validators().len() - ))); + validator_set.validators().len(), + )); } Ok(()) @@ -99,11 +95,10 @@ impl CommitValidator for ProdCommitValidator { }; if validator_set.validator(*validator_address) == None { - bail!(VerificationError::ImplementationSpecific(format!( - "Found a faulty signer ({}) not present in the validator set ({})", - validator_address, - self.hasher.hash_validator_set(validator_set) - ))); + return Err(VerificationError::faulty_signer( + *validator_address, + self.hasher.hash_validator_set(validator_set), + )); } } diff --git a/light-client/src/operations/voting_power.rs b/light-client/src/operations/voting_power.rs index 916962082..adf854dee 100644 --- a/light-client/src/operations/voting_power.rs +++ b/light-client/src/operations/voting_power.rs @@ -1,7 +1,6 @@ //! Provides an interface and default implementation for the `VotingPower` operation use crate::{ - bail, predicates::errors::VerificationError, types::{Commit, SignedHeader, TrustThreshold, ValidatorSet}, }; @@ -62,7 +61,7 @@ pub trait VotingPowerCalculator: Send + Sync { if trust_threshold.is_enough_power(voting_power.tallied, voting_power.total) { Ok(()) } else { - Err(VerificationError::NotEnoughTrust(voting_power)) + Err(VerificationError::not_enough_trust(voting_power)) } } @@ -80,7 +79,9 @@ pub trait VotingPowerCalculator: Send + Sync { if trust_threshold.is_enough_power(voting_power.tallied, voting_power.total) { Ok(()) } else { - Err(VerificationError::InsufficientSignersOverlap(voting_power)) + Err(VerificationError::insufficient_signers_overlap( + voting_power, + )) } } @@ -125,8 +126,8 @@ impl VotingPowerCalculator for ProdVotingPowerCalculator { for (signature, vote) in non_absent_votes { // Ensure we only count a validator's power once if seen_validators.contains(&vote.validator_address) { - bail!(VerificationError::DuplicateValidator( - vote.validator_address + return Err(VerificationError::duplicate_validator( + vote.validator_address, )); } else { seen_validators.insert(vote.validator_address); @@ -150,11 +151,11 @@ impl VotingPowerCalculator for ProdVotingPowerCalculator { .verify_signature(&sign_bytes, signed_vote.signature()) .is_err() { - bail!(VerificationError::InvalidSignature { - signature: signed_vote.signature().to_bytes(), - validator: Box::new(validator), + return Err(VerificationError::invalid_signature( + signed_vote.signature().to_bytes(), + Box::new(validator), sign_bytes, - }); + )); } // If the vote is neither absent nor nil, tally its power @@ -222,6 +223,7 @@ fn non_absent_vote( #[cfg(test)] mod tests { use super::*; + use crate::predicates::errors::VerificationErrorDetail; use crate::types::LightBlock; use tendermint::trust_threshold::TrustThresholdFraction; use tendermint_testgen::light_block::generate_signed_header; @@ -323,10 +325,9 @@ mod tests { trust_threshold, ); - let err = result_err.err().unwrap(); - match err { - VerificationError::InvalidSignature { .. } => {} - _ => panic!("unexpected error: {:?}", err), + match result_err { + Err(VerificationError(VerificationErrorDetail::InvalidSignature(_), _)) => {} + _ => panic!("expected InvalidSignature error"), } } @@ -346,10 +347,9 @@ mod tests { trust_threshold, ); - let err = result_err.err().unwrap(); - match err { - VerificationError::InvalidSignature { .. } => {} - _ => panic!("unexpected error: {:?}", err), + match result_err { + Err(VerificationError(VerificationErrorDetail::InvalidSignature(_), _)) => {} + _ => panic!("expected InvalidSignature error"), } } diff --git a/light-client/src/peer_list.rs b/light-client/src/peer_list.rs index dbd1c6fa5..a0e408e30 100644 --- a/light-client/src/peer_list.rs +++ b/light-client/src/peer_list.rs @@ -1,10 +1,6 @@ //! Provides a peer list for use within the `Supervisor` -use crate::{ - bail, - errors::{Error, ErrorKind}, - types::PeerId, -}; +use crate::{errors::Error, types::PeerId}; use contracts::{post, pre}; use std::collections::{BTreeSet, HashMap}; @@ -149,9 +145,9 @@ impl PeerList { self.witnesses.remove(&new_primary); Ok(new_primary) } else if let Some(err) = primary_error { - bail!(ErrorKind::NoWitnessLeft.context(err)) + Err(err) } else { - bail!(ErrorKind::NoWitnessLeft) + Err(Error::no_witnesses_left()) } } @@ -239,6 +235,7 @@ impl PeerListBuilder { #[cfg(test)] mod tests { use super::*; + use crate::errors::ErrorDetail; trait BTreeSetExt { fn to_vec(&self) -> Vec; @@ -307,10 +304,13 @@ mod tests { let mut peer_list = dummy_peer_list(); let _ = peer_list.replace_faulty_primary(None).unwrap(); let new_primary = peer_list.replace_faulty_primary(None); - assert_eq!( - new_primary.err().map(|e| e.kind().clone()), - Some(ErrorKind::NoWitnessLeft) - ); + match new_primary { + Err(Error(ErrorDetail::NoWitnessesLeft(_), _)) => {} + _ => panic!( + "expected NoWitnessesLeft error, instead got {:?}", + new_primary + ), + } } #[test] diff --git a/light-client/src/predicates.rs b/light-client/src/predicates.rs index 89057b7c7..a74340142 100644 --- a/light-client/src/predicates.rs +++ b/light-client/src/predicates.rs @@ -1,7 +1,6 @@ //! Predicates for light block validation and verification. use crate::{ - ensure, light_client::Options, operations::{CommitValidator, Hasher, VotingPowerCalculator}, types::{Header, LightBlock, SignedHeader, Time, TrustThreshold, ValidatorSet}, @@ -34,15 +33,14 @@ pub trait VerificationPredicates: Send + Sync { ) -> Result<(), VerificationError> { let validators_hash = hasher.hash_validator_set(&light_block.validators); - ensure!( - light_block.signed_header.header.validators_hash == validators_hash, - VerificationError::InvalidValidatorSet { - header_validators_hash: light_block.signed_header.header.validators_hash, + if light_block.signed_header.header.validators_hash == validators_hash { + Ok(()) + } else { + Err(VerificationError::invalid_validator_set( + light_block.signed_header.header.validators_hash, validators_hash, - } - ); - - Ok(()) + )) + } } /// Check that the hash of the next validator set in the header match the actual one. @@ -53,15 +51,14 @@ pub trait VerificationPredicates: Send + Sync { ) -> Result<(), VerificationError> { let next_validators_hash = hasher.hash_validator_set(&light_block.next_validators); - ensure!( - light_block.signed_header.header.next_validators_hash == next_validators_hash, - VerificationError::InvalidNextValidatorSet { - header_next_validators_hash: light_block.signed_header.header.next_validators_hash, + if light_block.signed_header.header.next_validators_hash == next_validators_hash { + Ok(()) + } else { + Err(VerificationError::invalid_next_validator_set( + light_block.signed_header.header.next_validators_hash, next_validators_hash, - } - ); - - Ok(()) + )) + } } /// Check that the hash of the header in the commit matches the actual one. @@ -72,15 +69,14 @@ pub trait VerificationPredicates: Send + Sync { ) -> Result<(), VerificationError> { let header_hash = hasher.hash_header(&signed_header.header); - ensure!( - header_hash == signed_header.commit.block_id.hash, - VerificationError::InvalidCommitValue { + if header_hash == signed_header.commit.block_id.hash { + Ok(()) + } else { + Err(VerificationError::invalid_commit_value( header_hash, - commit_hash: signed_header.commit.block_id.hash, - } - ); - - Ok(()) + signed_header.commit.block_id.hash, + )) + } } /// Validate the commit using the given commit validator. @@ -104,12 +100,12 @@ pub trait VerificationPredicates: Send + Sync { now: Time, ) -> Result<(), VerificationError> { let expires_at = trusted_header.time + trusting_period; - ensure!( - expires_at > now, - VerificationError::NotWithinTrustPeriod { expires_at, now } - ); - Ok(()) + if expires_at > now { + Ok(()) + } else { + Err(VerificationError::not_within_trust_period(expires_at, now)) + } } /// Check that the untrusted header is from past. @@ -119,15 +115,14 @@ pub trait VerificationPredicates: Send + Sync { clock_drift: Duration, now: Time, ) -> Result<(), VerificationError> { - ensure!( - untrusted_header.time < now + clock_drift, - VerificationError::HeaderFromTheFuture { - header_time: untrusted_header.time, - now - } - ); - - Ok(()) + if untrusted_header.time < now + clock_drift { + Ok(()) + } else { + Err(VerificationError::header_from_the_future( + untrusted_header.time, + now, + )) + } } /// Check that time passed monotonically between the trusted header and the untrusted one. @@ -136,15 +131,14 @@ pub trait VerificationPredicates: Send + Sync { untrusted_header: &Header, trusted_header: &Header, ) -> Result<(), VerificationError> { - ensure!( - untrusted_header.time > trusted_header.time, - VerificationError::NonMonotonicBftTime { - header_bft_time: untrusted_header.time, - trusted_header_bft_time: trusted_header.time, - } - ); - - Ok(()) + if untrusted_header.time > trusted_header.time { + Ok(()) + } else { + Err(VerificationError::non_monotonic_bft_time( + untrusted_header.time, + trusted_header.time, + )) + } } /// Check that the height increased between the trusted header and the untrusted one. @@ -155,15 +149,14 @@ pub trait VerificationPredicates: Send + Sync { ) -> Result<(), VerificationError> { let trusted_height = trusted_header.height; - ensure!( - untrusted_header.height > trusted_header.height, - VerificationError::NonIncreasingHeight { - got: untrusted_header.height, - expected: trusted_height.increment(), - } - ); - - Ok(()) + if untrusted_header.height > trusted_header.height { + Ok(()) + } else { + Err(VerificationError::non_increasing_height( + untrusted_header.height, + trusted_height.increment(), + )) + } } /// Check that there is enough validators overlap between the trusted validator set @@ -198,16 +191,16 @@ pub trait VerificationPredicates: Send + Sync { light_block: &LightBlock, trusted_state: &LightBlock, ) -> Result<(), VerificationError> { - ensure!( - light_block.signed_header.header.validators_hash - == trusted_state.signed_header.header.next_validators_hash, - VerificationError::InvalidNextValidatorSet { - header_next_validators_hash: light_block.signed_header.header.validators_hash, - next_validators_hash: trusted_state.signed_header.header.next_validators_hash, - } - ); - - Ok(()) + if light_block.signed_header.header.validators_hash + == trusted_state.signed_header.header.next_validators_hash + { + Ok(()) + } else { + Err(VerificationError::invalid_next_validator_set( + light_block.signed_header.header.validators_hash, + trusted_state.signed_header.header.next_validators_hash, + )) + } } } @@ -306,7 +299,10 @@ mod tests { Commit, Generator, Header, Validator, ValidatorSet, }; - use crate::predicates::{errors::VerificationError, ProdPredicates, VerificationPredicates}; + use crate::predicates::{ + errors::{VerificationError, VerificationErrorDetail}, + ProdPredicates, VerificationPredicates, + }; use crate::operations::{ Hasher, ProdCommitValidator, ProdHasher, ProdVotingPowerCalculator, VotingPowerTally, @@ -340,14 +336,13 @@ mod tests { // 2. ensure header with non-monotonic bft time fails let result_err = vp.is_monotonic_bft_time(&header_one, &header_two); - assert!(result_err.is_err()); - - // 3. expect to error with: VerificationError::NonMonotonicBftTime - let error = VerificationError::NonMonotonicBftTime { - header_bft_time: header_one.time, - trusted_header_bft_time: header_two.time, - }; - assert_eq!(result_err.err().unwrap(), error); + match result_err { + Err(VerificationError(VerificationErrorDetail::NonMonotonicBftTime(e), _)) => { + assert_eq!(e.header_bft_time, header_one.time); + assert_eq!(e.trusted_header_bft_time, header_two.time); + } + _ => panic!("expected NonMonotonicBftTime error"), + } } #[test] @@ -364,14 +359,14 @@ mod tests { // 2. ensure header with non-monotonic height fails let result_err = vp.is_monotonic_height(&header_one, &header_two); - assert!(result_err.is_err()); - - // 3. expect to error with: VerificationError::NonMonotonicBftTime - let error = VerificationError::NonIncreasingHeight { - got: header_one.height, - expected: header_two.height.increment(), - }; - assert_eq!(result_err.err().unwrap(), error); + + match result_err { + Err(VerificationError(VerificationErrorDetail::NonIncreasingHeight(e), _)) => { + assert_eq!(e.got, header_one.height); + assert_eq!(e.expected, header_two.height.increment()); + } + _ => panic!("expected NonIncreasingHeight error"), + } } #[test] @@ -392,12 +387,15 @@ mod tests { trusting_period = Duration::new(0, 1); let result_err = vp.is_within_trust_period(&header, trusting_period, now); - assert!(result_err.is_err()); - // 3. ensure it fails with: VerificationError::NotWithinTrustPeriod let expires_at = header.time + trusting_period; - let error = VerificationError::NotWithinTrustPeriod { expires_at, now }; - assert_eq!(result_err.err().unwrap(), error); + match result_err { + Err(VerificationError(VerificationErrorDetail::NotWithinTrustPeriod(e), _)) => { + assert_eq!(e.expires_at, expires_at); + assert_eq!(e.now, now); + } + _ => panic!("expected NotWithinTrustPeriod error"), + } } #[test] @@ -417,15 +415,13 @@ mod tests { let now = Time::now().sub(one_second * 15); let result_err = vp.is_header_from_past(&header, one_second, now); - assert!(result_err.is_err()); - - // 3. ensure it fails with: VerificationError::HeaderFromTheFuture - let error = VerificationError::HeaderFromTheFuture { - header_time: header.time, - now, - }; - - assert_eq!(result_err.err().unwrap(), error); + match result_err { + Err(VerificationError(VerificationErrorDetail::HeaderFromTheFuture(e), _)) => { + assert_eq!(e.header_time, header.time); + assert_eq!(e.now, now); + } + _ => panic!("expected HeaderFromTheFuture error"), + } } #[test] @@ -456,31 +452,37 @@ mod tests { let val_sets_match_err = vp.validator_sets_match(&light_block, &hasher); - // ensure it fails - assert!(val_sets_match_err.is_err()); - - let val_set_error = VerificationError::InvalidValidatorSet { - header_validators_hash: light_block.signed_header.header.validators_hash, - validators_hash: hasher.hash_validator_set(&light_block.validators), - }; - - // ensure it fails with VerificationError::InvalidValidatorSet - assert_eq!(val_sets_match_err.err().unwrap(), val_set_error); + match val_sets_match_err { + Err(VerificationError(VerificationErrorDetail::InvalidValidatorSet(e), _)) => { + assert_eq!( + e.header_validators_hash, + light_block.signed_header.header.validators_hash + ); + assert_eq!( + e.validators_hash, + hasher.hash_validator_set(&light_block.validators) + ); + } + _ => panic!("expected InvalidValidatorSet error"), + } // 2. For predicate: next_validator_sets_match light_block.next_validators = bad_validator_set; let next_val_sets_match_err = vp.next_validators_match(&light_block, &hasher); - // ensure it fails - assert!(next_val_sets_match_err.is_err()); - - let next_val_set_error = VerificationError::InvalidNextValidatorSet { - header_next_validators_hash: light_block.signed_header.header.next_validators_hash, - next_validators_hash: hasher.hash_validator_set(&light_block.next_validators), - }; - - // ensure it fails with VerificationError::InvalidNextValidatorSet - assert_eq!(next_val_sets_match_err.err().unwrap(), next_val_set_error); + match next_val_sets_match_err { + Err(VerificationError(VerificationErrorDetail::InvalidNextValidatorSet(e), _)) => { + assert_eq!( + e.header_next_validators_hash, + light_block.signed_header.header.next_validators_hash + ); + assert_eq!( + e.next_validators_hash, + hasher.hash_validator_set(&light_block.next_validators) + ); + } + _ => panic!("expected InvalidNextValidatorSet error"), + } } #[test] @@ -505,16 +507,16 @@ mod tests { .unwrap(); let result_err = vp.header_matches_commit(&signed_header, &hasher); - assert!(result_err.is_err()); - - // 3. ensure it fails with: VerificationError::InvalidCommitValue + // 3. ensure it fails with: VerificationVerificationError::InvalidCommitValue let header_hash = hasher.hash_header(&signed_header.header); - let error = VerificationError::InvalidCommitValue { - header_hash, - commit_hash: signed_header.commit.block_id.hash, - }; - assert_eq!(result_err.err().unwrap(), error); + match result_err { + Err(VerificationError(VerificationErrorDetail::InvalidCommitValue(e), _)) => { + assert_eq!(e.header_hash, header_hash); + assert_eq!(e.commit_hash, signed_header.commit.block_id.hash); + } + _ => panic!("expected InvalidCommitValue error"), + } } #[test] @@ -539,14 +541,11 @@ mod tests { signed_header.commit.signatures = vec![]; let mut result_err = vp.valid_commit(&signed_header, &val_set, &commit_validator); - assert!(result_err.is_err()); - let mut error = - VerificationError::ImplementationSpecific("no signatures for commit".to_string()); - - // ensure it fails with: - // VerificationError::ImplementationSpecific("no signatures for commit") - assert_eq!(result_err.err().unwrap(), error); + match result_err { + Err(VerificationError(VerificationErrorDetail::NoSignatureForCommit(_), _)) => {} + _ => panic!("expected ImplementationSpecific error"), + } // 3. commit.signatures.len() != validator_set.validators().len() // must return error @@ -554,16 +553,14 @@ mod tests { signed_header.commit.signatures = bad_sigs.clone(); result_err = vp.valid_commit(&signed_header, &val_set, &commit_validator); - assert!(result_err.is_err()); - - error = VerificationError::ImplementationSpecific(format!( - "pre-commit length: {} doesn't match validator length: {}", - signed_header.commit.signatures.len(), - val_set.validators().len() - )); - // ensure it fails with the expected error (as above) - assert_eq!(result_err.err().unwrap(), error); + match result_err { + Err(VerificationError(VerificationErrorDetail::MismatchPreCommitLength(e), _)) => { + assert_eq!(e.pre_commit_length, signed_header.commit.signatures.len()); + assert_eq!(e.validator_length, val_set.validators().len()); + } + _ => panic!("expected MismatchPreCommitLength error"), + } // 4. commit.BlockIdFlagAbsent - should be "Ok" bad_sigs.push(CommitSig::BlockIdFlagAbsent); @@ -589,23 +586,28 @@ mod tests { &val_set_with_faulty_signer, &commit_validator, ); - assert!(result_err.is_err()); - - error = VerificationError::ImplementationSpecific(format!( - "Found a faulty signer ({}) not present in the validator set ({})", - signed_header - .commit - .signatures - .iter() - .last() - .unwrap() - .validator_address() - .unwrap(), - hasher.hash_validator_set(&val_set_with_faulty_signer) - )); - // ensure it fails with the expected error (as above) - assert_eq!(result_err.err().unwrap(), error); + match result_err { + Err(VerificationError(VerificationErrorDetail::FaultySigner(e), _)) => { + assert_eq!( + e.signer, + signed_header + .commit + .signatures + .iter() + .last() + .unwrap() + .validator_address() + .unwrap() + ); + + assert_eq!( + e.validator_set, + hasher.hash_validator_set(&val_set_with_faulty_signer) + ); + } + _ => panic!("expected FaultySigner error"), + } } #[test] @@ -635,15 +637,19 @@ mod tests { let result_err = vp.valid_next_validator_set(&light_block3, &light_block2); - assert!(result_err.is_err()); - - let error = VerificationError::InvalidNextValidatorSet { - header_next_validators_hash: light_block3.signed_header.header.validators_hash, - next_validators_hash: light_block2.signed_header.header.next_validators_hash, - }; - - // ensure it fails with the expected error (as above) - assert_eq!(result_err.err().unwrap(), error); + match result_err { + Err(VerificationError(VerificationErrorDetail::InvalidNextValidatorSet(e), _)) => { + assert_eq!( + e.header_next_validators_hash, + light_block3.signed_header.header.validators_hash + ); + assert_eq!( + e.next_validators_hash, + light_block2.signed_header.header.next_validators_hash + ); + } + _ => panic!("expected InvalidNextValidatorSet error"), + } } #[test] @@ -686,16 +692,19 @@ mod tests { &voting_power_calculator, ); - assert!(result_err.is_err()); - - let error = VerificationError::NotEnoughTrust(VotingPowerTally { - total: 200, - tallied: 100, - trust_threshold, - }); - - // ensure it fails with the expected error (as above) - assert_eq!(result_err.err().unwrap(), error); + match result_err { + Err(VerificationError(VerificationErrorDetail::NotEnoughTrust(e), _)) => { + assert_eq!( + e.tally, + VotingPowerTally { + total: 200, + tallied: 100, + trust_threshold, + } + ); + } + _ => panic!("expected NotEnoughTrust error"), + } } #[test] @@ -725,16 +734,20 @@ mod tests { &voting_power_calculator, ); - assert!(result_err.is_err()); - let trust_threshold = TrustThreshold::TWO_THIRDS; - let error = VerificationError::InsufficientSignersOverlap(VotingPowerTally { - total: 100, - tallied: 50, - trust_threshold, - }); - - // ensure it fails with the expected error (as above) - assert_eq!(result_err.err().unwrap(), error); + + match result_err { + Err(VerificationError(VerificationErrorDetail::InsufficientSignersOverlap(e), _)) => { + assert_eq!( + e.tally, + VotingPowerTally { + total: 100, + tallied: 50, + trust_threshold, + } + ); + } + _ => panic!("expected InsufficientSignersOverlap error"), + } } } diff --git a/light-client/src/predicates/errors.rs b/light-client/src/predicates/errors.rs index 99c02fb34..9ab9112fa 100644 --- a/light-client/src/predicates/errors.rs +++ b/light-client/src/predicates/errors.rs @@ -1,132 +1,179 @@ //! Errors which may be raised when verifying a `LightBlock` -use anomaly::{BoxError, Context}; +use flex_error::define_error; use serde::{Deserialize, Serialize}; -use thiserror::Error; +use std::time::Duration; use crate::errors::ErrorExt; use crate::operations::voting_power::VotingPowerTally; use crate::types::{Hash, Height, Time, Validator, ValidatorAddress}; +use tendermint::account::Id; + +define_error! { + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + VerificationError { + HeaderFromTheFuture + { + header_time: Time, + now: Time, + } + | e | { + format_args!("header from the future: header_time={0} now={1}", + e.header_time, e.now) + }, + + ImplementationSpecific + { + detail: String, + } + | e | { + format_args!("implementation specific: {0}", + e.detail) + }, + + NotEnoughTrust + { + tally: VotingPowerTally, + } + | e | { + format_args!("not enough trust because insufficient validators overlap: {0}", + e.tally) + }, + + InsufficientSignersOverlap + { + tally: VotingPowerTally, + } + | e | { + format_args!("insufficient signers overlap: {0}", + e.tally) + }, + + DuplicateValidator + { + address: ValidatorAddress, + } + | e | { + format_args!("duplicate validator with address {0}", + e.address) + }, + + InvalidSignature + { + signature: Vec, + validator: Box, + sign_bytes: Vec, + } + | e | { + format_args!("Couldn't verify signature `{:?}` with validator `{:?}` on sign_bytes `{:?}`", + e.signature, e.validator, e.sign_bytes) + }, + + InvalidCommitValue + { + header_hash: Hash, + commit_hash: Hash, + } + | e | { + format_args!("invalid commit value: header_hash={0} commit_hash={1}", + e.header_hash, e.commit_hash) + }, + + InvalidNextValidatorSet + { + header_next_validators_hash: Hash, + next_validators_hash: Hash, + } + | e | { + format_args!("invalid next validator set: header_next_validators_hash={0} next_validators_hash={1}", + e.header_next_validators_hash, e.next_validators_hash) + }, + + InvalidValidatorSet + { + header_validators_hash: Hash, + validators_hash: Hash, + } + | e | { + format_args!("invalid validator set: header_validators_hash={0} validators_hash={1}", + e.header_validators_hash, e.validators_hash) + }, + + NonIncreasingHeight + { + got: Height, + expected: Height, + } + | e | { + format_args!("non increasing height: got={0} expected={1}", + e.got, e.expected) + }, + + NonMonotonicBftTime + { + header_bft_time: Time, + trusted_header_bft_time: Time, + } + | e | { + format_args!("non monotonic BFT time: header_bft_time={0} trusted_header_bft_time={1}", + e.header_bft_time, e.trusted_header_bft_time) + }, + + NotWithinTrustPeriod + { + expires_at: Time, + now: Time, + } + | e | { + format_args!("not withing trusting period: expires_at={0} now={1}", + e.expires_at, e.now) + }, + + NoSignatureForCommit + | _ | { "no signatures for commit" }, + + MismatchPreCommitLength + { + pre_commit_length: usize, + validator_length: usize, + } + | e | { + format_args!( + "pre-commit length: {} doesn't match validator length: {}", + e.pre_commit_length, + e.validator_length + ) + }, + + FaultySigner + { + signer: Id, + validator_set: Hash + } + | e | { + format_args!( + "Found a faulty signer ({}) not present in the validator set ({})", + e.signer, + e.validator_set + ) + }, -/// The various errors which can be raised by the verifier component, -/// when validating or verifying a light block. -#[derive(Debug, Clone, Error, PartialEq, Serialize, Deserialize, Eq)] -pub enum VerificationError { - /// The header is from the future - #[error("header from the future: header_time={header_time} now={now}")] - HeaderFromTheFuture { - /// Time in the header - header_time: Time, - /// Current time - now: Time, - }, - - /// Implementation specific error, for the purpose of extensibility - #[error("implementation specific: {0}")] - ImplementationSpecific(String), - - /// Not enough trust because insufficient validators overlap - #[error("not enough trust because insufficient validators overlap: {0}")] - NotEnoughTrust(VotingPowerTally), - - /// Insufficient signers overlap - #[error("insufficient signers overlap: {0}")] - InsufficientSignersOverlap(VotingPowerTally), - - /// Duplicate validator in commit signatures - #[error("duplicate validator with address {0}")] - DuplicateValidator(ValidatorAddress), - - /// Invalid commit signature - #[error("Couldn't verify signature `{signature:?}` with validator `{validator:?}` on sign_bytes `{sign_bytes:?}`")] - InvalidSignature { - /// Signature as a byte array - signature: Vec, - /// Validator which provided the signature - validator: Box, - /// Bytes which were signed - sign_bytes: Vec, - }, - - /// Invalid commit - #[error("invalid commit value: header_hash={header_hash} commit_hash={commit_hash}")] - InvalidCommitValue { - /// Header hash - #[serde(with = "tendermint::serializers::hash")] - header_hash: Hash, - /// Commit hash - #[serde(with = "tendermint::serializers::hash")] - commit_hash: Hash, - }, - - /// Hash mismatch for the next validator set - #[error("invalid next validator set: header_next_validators_hash={header_next_validators_hash} next_validators_hash={next_validators_hash}")] - InvalidNextValidatorSet { - /// Next validator set hash - #[serde(with = "tendermint::serializers::hash")] - header_next_validators_hash: Hash, - /// Validator set hash - #[serde(with = "tendermint::serializers::hash")] - next_validators_hash: Hash, - }, - - /// Hash mismatch for the validator set - #[error("invalid validator set: header_validators_hash={header_validators_hash} validators_hash={validators_hash}")] - InvalidValidatorSet { - /// Hash of validator set stored in header - #[serde(with = "tendermint::serializers::hash")] - header_validators_hash: Hash, - /// Actual hash of validator set in header - #[serde(with = "tendermint::serializers::hash")] - validators_hash: Hash, - }, - - /// Unexpected header of non-increasing height compared to what was expected - #[error("non increasing height: got={got} expected={expected}")] - NonIncreasingHeight { - /// Actual height of header - got: Height, - /// Expected minimum height - expected: Height, - }, - - /// BFT Time between the trusted state and a header does not increase monotonically - #[error("non monotonic BFT time: header_bft_time={header_bft_time} trusted_header_bft_time={trusted_header_bft_time}")] - NonMonotonicBftTime { - /// BFT time of the untrusted header - header_bft_time: Time, - /// BFT time of the trusted header - trusted_header_bft_time: Time, - }, - - /// Trusted state not within the trusting period - #[error("not withing trusting period: expires_at={expires_at} now={now}")] - NotWithinTrustPeriod { - /// Expiration time of the header - expires_at: Time, - /// Current time - now: Time, - }, -} - -impl VerificationError { - /// Add additional context (i.e. include a source error and capture a backtrace). - /// You can convert the resulting `Context` into an `Error` by calling `.into()`. - pub fn context(self, source: impl Into) -> Context { - Context::new(self, Some(source.into())) } } -impl ErrorExt for VerificationError { - fn not_enough_trust(&self) -> bool { - matches!(self, Self::NotEnoughTrust { .. }) +impl ErrorExt for VerificationErrorDetail { + fn not_enough_trust(&self) -> Option { + match &self { + Self::NotEnoughTrust(e) => Some(e.tally), + _ => None, + } } fn has_expired(&self) -> bool { matches!(self, Self::NotWithinTrustPeriod { .. }) } - fn is_timeout(&self) -> bool { - false + fn is_timeout(&self) -> Option { + None } } diff --git a/light-client/src/store/sled/utils.rs b/light-client/src/store/sled/utils.rs index 77b910622..aefe3d7d6 100644 --- a/light-client/src/store/sled/utils.rs +++ b/light-client/src/store/sled/utils.rs @@ -6,7 +6,7 @@ use std::marker::PhantomData; use serde::{de::DeserializeOwned, Serialize}; -use crate::errors::{Error, ErrorKind}; +use crate::errors::Error; use crate::types::Height; /// Provides a view over the database for storing key/value pairs at the given prefix. @@ -40,15 +40,11 @@ where /// Get the value associated with the given height within this tree pub fn get(&self, height: Height) -> Result, Error> { let key = key_bytes(height); - let value = self - .tree - .get(key) - .map_err(|e| ErrorKind::Store.context(e))?; + let value = self.tree.get(key).map_err(Error::sled)?; match value { Some(bytes) => { - let value = - serde_cbor::from_slice(&bytes).map_err(|e| ErrorKind::Store.context(e))?; + let value = serde_cbor::from_slice(&bytes).map_err(Error::serde_cbor)?; Ok(value) } None => Ok(None), @@ -59,10 +55,7 @@ where pub fn contains_key(&self, height: Height) -> Result { let key = key_bytes(height); - let exists = self - .tree - .contains_key(key) - .map_err(|e| ErrorKind::Store.context(e))?; + let exists = self.tree.contains_key(key).map_err(Error::sled)?; Ok(exists) } @@ -70,11 +63,9 @@ where /// Insert a value associated with a height within this tree pub fn insert(&self, height: Height, value: &V) -> Result<(), Error> { let key = key_bytes(height); - let bytes = serde_cbor::to_vec(&value).map_err(|e| ErrorKind::Store.context(e))?; + let bytes = serde_cbor::to_vec(&value).map_err(Error::serde_cbor)?; - self.tree - .insert(key, bytes) - .map_err(|e| ErrorKind::Store.context(e))?; + self.tree.insert(key, bytes).map_err(Error::sled)?; Ok(()) } @@ -83,9 +74,7 @@ where pub fn remove(&self, height: Height) -> Result<(), Error> { let key = key_bytes(height); - self.tree - .remove(key) - .map_err(|e| ErrorKind::Store.context(e))?; + self.tree.remove(key).map_err(Error::sled)?; Ok(()) } diff --git a/light-client/src/supervisor.rs b/light-client/src/supervisor.rs index 0d356d0b1..df396d2a1 100644 --- a/light-client/src/supervisor.rs +++ b/light-client/src/supervisor.rs @@ -4,8 +4,7 @@ use crossbeam_channel as channel; use tendermint::evidence::{ConflictingHeadersEvidence, Evidence}; -use crate::bail; -use crate::errors::{Error, ErrorKind}; +use crate::errors::Error; use crate::evidence::EvidenceReporter; use crate::fork_detector::{Fork, ForkDetection, ForkDetector}; use crate::light_client::LightClient; @@ -215,7 +214,7 @@ impl Supervisor { Ok(verified_block) => { let trusted_block = primary .latest_trusted() - .ok_or(ErrorKind::NoTrustedState(Status::Trusted))?; + .ok_or_else(|| Error::no_trusted_state(Status::Trusted))?; // Perform fork detection with the highest verified block and the trusted block. let outcome = self.detect_forks(&verified_block, &trusted_block)?; @@ -226,7 +225,7 @@ impl Supervisor { let forked = self.process_forks(forks)?; if !forked.is_empty() { // Fork detected, exiting - bail!(ErrorKind::ForkDetected(forked)) + return Err(Error::fork_detected(forked)); } // If there were no hard forks, perform verification again @@ -297,7 +296,7 @@ impl Supervisor { self.evidence_reporter .report(Evidence::ConflictingHeaders(Box::new(evidence)), provider) - .map_err(ErrorKind::Io)?; + .map_err(Error::io)?; Ok(()) } @@ -309,7 +308,7 @@ impl Supervisor { trusted_block: &LightBlock, ) -> Result { if self.peers.witnesses_ids().is_empty() { - bail!(ErrorKind::NoWitnesses); + return Err(Error::no_witnesses()); } let witnesses = self @@ -328,28 +327,28 @@ impl Supervisor { /// This method should typically be called within a new thread with `std::thread::spawn`. pub fn run(mut self) -> Result<(), Error> { loop { - let event = self.receiver.recv().map_err(ErrorKind::from)?; + let event = self.receiver.recv().map_err(Error::recv)?; match event { HandleInput::LatestTrusted(sender) => { let outcome = self.latest_trusted(); - sender.send(outcome).map_err(ErrorKind::from)?; + sender.send(outcome).map_err(Error::send)?; } HandleInput::Terminate(sender) => { - sender.send(()).map_err(ErrorKind::from)?; + sender.send(()).map_err(Error::send)?; return Ok(()); } HandleInput::VerifyToTarget(height, sender) => { let outcome = self.verify_to_target(height); - sender.send(outcome).map_err(ErrorKind::from)?; + sender.send(outcome).map_err(Error::send)?; } HandleInput::VerifyToHighest(sender) => { let outcome = self.verify_to_highest(); - sender.send(outcome).map_err(ErrorKind::from)?; + sender.send(outcome).map_err(Error::send)?; } HandleInput::GetStatus(sender) => { let outcome = self.latest_status(); - sender.send(outcome).map_err(ErrorKind::from)?; + sender.send(outcome).map_err(Error::send)?; } } } @@ -377,9 +376,9 @@ impl SupervisorHandle { let (sender, receiver) = channel::bounded::>(1); let event = make_event(sender); - self.sender.send(event).map_err(ErrorKind::from)?; + self.sender.send(event).map_err(Error::send)?; - receiver.recv().map_err(ErrorKind::from)? + receiver.recv().map_err(Error::recv)? } } @@ -389,17 +388,17 @@ impl Handle for SupervisorHandle { self.sender .send(HandleInput::LatestTrusted(sender)) - .map_err(ErrorKind::from)?; + .map_err(Error::send)?; - Ok(receiver.recv().map_err(ErrorKind::from)?) + receiver.recv().map_err(Error::recv) } fn latest_status(&self) -> Result { let (sender, receiver) = channel::bounded::(1); self.sender .send(HandleInput::GetStatus(sender)) - .map_err(ErrorKind::from)?; - Ok(receiver.recv().map_err(ErrorKind::from)?) + .map_err(Error::send)?; + receiver.recv().map_err(Error::recv) } fn verify_to_highest(&self) -> Result { @@ -415,21 +414,21 @@ impl Handle for SupervisorHandle { self.sender .send(HandleInput::Terminate(sender)) - .map_err(ErrorKind::from)?; + .map_err(Error::send)?; - Ok(receiver.recv().map_err(ErrorKind::from)?) + receiver.recv().map_err(Error::recv) } } #[cfg(test)] mod tests { use super::*; - use crate::components::io::IoError; + use crate::errors::{Error, ErrorDetail}; use crate::light_client::Options; use crate::operations::ProdHasher; use crate::{ components::{ - io::{AtHeight, Io}, + io::{self, AtHeight, Io}, scheduler, verifier::ProdVerifier, }, @@ -442,8 +441,10 @@ mod tests { use tendermint::block::Height; use tendermint::evidence::Duration as DurationStr; use tendermint::trust_threshold::TrustThresholdFraction; - use tendermint_rpc as rpc; - use tendermint_rpc::error::Code; + use tendermint_rpc::{ + self as rpc, + response_error::{Code, ResponseError}, + }; use tendermint_testgen::helpers::get_time; use tendermint_testgen::{ Commit, Generator, Header, LightBlock as TestgenLightBlock, LightChain, ValidatorSet, @@ -620,10 +621,10 @@ mod tests { let (result, _) = run_bisection_test(peer_list, 10); - let expected_err = ErrorKind::NoWitnesses; - let got_err = result.err().unwrap(); - - assert_eq!(&expected_err, got_err.kind()); + match result { + Err(Error(ErrorDetail::NoWitnesses(_), _)) => {} + _ => panic!("expected NoWitnesses error, instead got {:?}", result), + } } #[test] @@ -643,13 +644,18 @@ mod tests { let (result, _) = run_bisection_test(peer_list, 10); - let expected_err = ErrorKind::Io(IoError::RpcError(rpc::Error::new( - Code::InvalidRequest, - None, - ))); - let got_err = result.err().unwrap(); - - assert_eq!(&expected_err, got_err.kind()); + match result { + Err(Error(ErrorDetail::Io(e), _)) => match e.source { + io::IoErrorDetail::Rpc(e) => match e.source { + rpc::error::ErrorDetail::Response(e) => { + assert_eq!(e.source, ResponseError::new(Code::InvalidRequest, None)) + } + _ => panic!("expected Response error"), + }, + _ => panic!("expected Rpc error"), + }, + _ => panic!("expected Io error"), + } } #[test] @@ -667,10 +673,25 @@ mod tests { let (result, _) = run_bisection_test(peer_list, 10); - let expected_err = ErrorKind::NoWitnessLeft; - let got_err = result.err().unwrap(); - - assert_eq!(&expected_err, got_err.kind()); + // FIXME: currently this test does not test what it is supposed to test, + // because MockIo returns an InvalidRequest error. This was previously + // treated as a NoWitnessLeft error, which was misclassified. + match result { + Err(Error(ErrorDetail::Io(e), _)) => match e.source { + crate::components::io::IoErrorDetail::Rpc(e) => match e.source { + rpc::error::ErrorDetail::Response(e) => { + assert_eq!(e.source.code(), rpc::Code::InvalidRequest) + } + _ => { + panic!("expected Response error, instead got {:?}", e) + } + }, + _ => { + panic!("expected Rpc error, instead got {:?}", e) + } + }, + _ => panic!("expected Io error, instead got {:?}", result), + } } #[test] @@ -699,14 +720,14 @@ mod tests { None, ); - let peer_list = make_peer_list(Some(primary), Some(vec![witness.clone()]), get_time(11)); + let peer_list = make_peer_list(Some(primary), Some(vec![witness]), get_time(11)); let (result, _) = run_bisection_test(peer_list, 5); - let expected_err = ErrorKind::ForkDetected(vec![witness[0].provider]); - let got_err = result.err().unwrap(); - - assert_eq!(&expected_err, got_err.kind()); + match result { + Err(Error(ErrorDetail::ForkDetected(_), _)) => {} + _ => panic!("expected ForkDetected error"), + } } #[test] diff --git a/light-client/src/tests.rs b/light-client/src/tests.rs index 9cf6e30ca..82d2c3b6e 100644 --- a/light-client/src/tests.rs +++ b/light-client/src/tests.rs @@ -113,10 +113,11 @@ impl Io for MockIo { AtHeight::At(height) => height, }; - self.light_blocks - .get(&height) - .cloned() - .ok_or_else(|| rpc::Error::new((-32600).into(), None).into()) + self.light_blocks.get(&height).cloned().ok_or_else(|| { + IoError::rpc(rpc::Error::response( + rpc::response_error::ResponseError::new((-32600).into(), None), + )) + }) } } diff --git a/light-client/src/utils/block_on.rs b/light-client/src/utils/block_on.rs index 77a7f7fc9..6cd565a88 100644 --- a/light-client/src/utils/block_on.rs +++ b/light-client/src/utils/block_on.rs @@ -14,11 +14,11 @@ where let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() - .map_err(|_| IoError::Runtime)?; + .map_err(IoError::runtime)?; if let Some(timeout) = timeout { let task = async { tokio::time::timeout(timeout, f).await }; - rt.block_on(task).map_err(|_| IoError::Timeout(timeout)) + rt.block_on(task).map_err(|e| IoError::timeout(timeout, e)) } else { Ok(rt.block_on(f)) } diff --git a/p2p/Cargo.toml b/p2p/Cargo.toml index 3cfbfb457..d5d9e31fa 100644 --- a/p2p/Cargo.toml +++ b/p2p/Cargo.toml @@ -23,7 +23,12 @@ description = """ test = false [features] +default = ["std", "eyre_tracer"] +eyre_tracer = ["flex-error/eyre_tracer"] amino = ["prost-amino", "prost-amino-derive"] +std = [ + "flex-error/std" +] [dependencies] chacha20poly1305 = "0.8" @@ -36,9 +41,11 @@ prost = "0.7" rand_core = { version = "0.5", features = ["std"] } sha2 = "0.9" subtle = "2" -thiserror = "1" x25519-dalek = "1.1" zeroize = "1" +signature = "1.3.0" +aead = "0.4.1" +flex-error = { version = "0.4.1", default-features = false } # path dependencies tendermint = { path = "../tendermint", version = "0.21.0" } diff --git a/p2p/src/error.rs b/p2p/src/error.rs index 4f1cc20a7..3f609f524 100644 --- a/p2p/src/error.rs +++ b/p2p/src/error.rs @@ -1,19 +1,77 @@ //! Error types -use thiserror::Error; - -/// Kinds of errors -#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)] -pub enum Error { - /// Cryptographic operation failed - #[error("cryptographic error")] - Crypto, - - /// Malformatted or otherwise invalid cryptographic key - #[error("invalid key")] - InvalidKey, - - /// Network protocol-related errors - #[error("protocol error")] - Protocol, +use flex_error::{define_error, DisplayError, TraceError}; +use prost::DecodeError; +use signature::Error as SignatureError; + +#[cfg(feature = "amino")] +type AminoDecodeError = TraceError; + +#[cfg(not(feature = "amino"))] +type AminoDecodeError = flex_error::NoSource; + +define_error! { + Error { + Crypto + | _ | { "cryptographic error" }, + + InvalidKey + | _ | { "invalid key" }, + + LowOrderKey + | _ | { "low-order points found (potential MitM attack!)" }, + + Protocol + | _ | { "protocol error" }, + + MalformedHandshake + | _ | { "malformed handshake message (protocol version mismatch?)" }, + + Io + [ TraceError ] + | _ | { "io error" }, + + Decode + [ TraceError ] + | _ | { "malformed handshake message (protocol version mismatch?)" }, + + AminoDecode + [ AminoDecodeError ] + | _ | { "malformed handshake message (protocol version mismatch?)" }, + + MissingSecret + | _ | { "missing secret: forgot to call Handshake::new?" }, + + MissingKey + | _ | { "public key missing" }, + + Signature + [ TraceError ] + | _ | { "signature error" }, + + UnsupportedKey + | _ | { "secp256k1 is not supported" }, + + Aead + [ DisplayError ] + | _ | { "aead error" }, + + ShortCiphertext + { tag_size: usize } + | e | { format_args!("ciphertext must be at least as long as a MAC tag {}", e.tag_size) }, + + SmallOutputBuffer + | _ | { "output buffer is too small" }, + + TransportClone + { detail: String } + | e | { format_args!("failed to clone underlying transport: {}", e.detail) } + + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Self::io(e) + } } diff --git a/p2p/src/lib.rs b/p2p/src/lib.rs index 236c09609..b8b78e098 100644 --- a/p2p/src/lib.rs +++ b/p2p/src/lib.rs @@ -16,7 +16,6 @@ clippy::nursery, clippy::pedantic, clippy::unwrap_used, - missing_docs, unused_import_braces, unused_qualifications )] diff --git a/p2p/src/secret_connection.rs b/p2p/src/secret_connection.rs index 063b1de19..18581a1b6 100644 --- a/p2p/src/secret_connection.rs +++ b/p2p/src/secret_connection.rs @@ -8,16 +8,15 @@ use std::slice; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use crate::error::Error; use chacha20poly1305::{ aead::{generic_array::GenericArray, AeadInPlace, NewAead}, ChaCha20Poly1305, }; use ed25519_dalek::{self as ed25519, Signer, Verifier}; -use eyre::{eyre, Result, WrapErr}; use merlin::Transcript; use rand_core::OsRng; use subtle::ConstantTimeEq; -use thiserror::Error; use x25519_dalek::{EphemeralSecret, PublicKey as EphemeralPublic}; use tendermint_proto as proto; @@ -48,22 +47,6 @@ pub const DATA_MAX_SIZE: usize = 1024; const DATA_LEN_SIZE: usize = 4; const TOTAL_FRAME_SIZE: usize = DATA_MAX_SIZE + DATA_LEN_SIZE; -/// Kinds of errors -#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)] -pub enum Error { - /// Cryptographic operation failed - #[error("cryptographic error")] - Crypto, - - /// Malformatted or otherwise invalid cryptographic key - #[error("invalid key")] - InvalidKey, - - /// Network protocol-related errors - #[error("protocol error")] - Protocol, -} - /// Handshake is a process of establishing the `SecretConnection` between two peers. /// [Specification](https://github.com/tendermint/spec/blob/master/spec/p2p/peer.md#authenticated-encryption-handshake) pub struct Handshake { @@ -122,10 +105,10 @@ impl Handshake { pub fn got_key( &mut self, remote_eph_pubkey: EphemeralPublic, - ) -> Result> { + ) -> Result, Error> { let local_eph_privkey = match self.state.local_eph_privkey.take() { Some(key) => key, - None => return Err(eyre!("forgot to call Handshake::new?")), + None => return Err(Error::missing_secret()), }; let local_eph_pubkey = EphemeralPublic::from(&local_eph_privkey); @@ -142,8 +125,7 @@ impl Handshake { // - https://github.com/tendermint/kms/issues/142 // - https://eprint.iacr.org/2019/526.pdf if shared_secret.as_bytes().ct_eq(&[0x00; 32]).unwrap_u8() == 1 { - return Err(Error::InvalidKey) - .wrap_err("low-order points found (potential MitM attack!)"); + return Err(Error::low_order_key()); } // Sort by lexical order. @@ -190,28 +172,33 @@ impl Handshake { /// # Errors /// /// * if signature scheme isn't supported - pub fn got_signature(&mut self, auth_sig_msg: proto::p2p::AuthSigMessage) -> Result { - let remote_pubkey = auth_sig_msg + pub fn got_signature( + &mut self, + auth_sig_msg: proto::p2p::AuthSigMessage, + ) -> Result { + let pk_sum = auth_sig_msg .pub_key - .and_then(|pk| match pk.sum? { - proto::crypto::public_key::Sum::Ed25519(ref bytes) => { - ed25519::PublicKey::from_bytes(bytes).ok() - } - proto::crypto::public_key::Sum::Secp256k1(_) => None, - }) - .ok_or(Error::Crypto)?; + .and_then(|key| key.sum) + .ok_or_else(Error::missing_key)?; + + let remote_pubkey = match pk_sum { + proto::crypto::public_key::Sum::Ed25519(ref bytes) => { + ed25519::PublicKey::from_bytes(bytes).map_err(Error::signature) + } + proto::crypto::public_key::Sum::Secp256k1(_) => Err(Error::unsupported_key()), + }?; let remote_sig = - ed25519::Signature::try_from(auth_sig_msg.sig.as_slice()).map_err(|_| Error::Crypto)?; + ed25519::Signature::try_from(auth_sig_msg.sig.as_slice()).map_err(Error::signature)?; if self.protocol_version.has_transcript() { remote_pubkey .verify(&self.state.sc_mac, &remote_sig) - .map_err(|_| Error::Crypto)?; + .map_err(Error::signature)?; } else { remote_pubkey .verify(&self.state.kdf.challenge, &remote_sig) - .map_err(|_| Error::Crypto)?; + .map_err(Error::signature)?; } // We've authorized. @@ -291,7 +278,7 @@ impl SecretConnection { mut io_handler: IoHandler, local_privkey: ed25519::Keypair, protocol_version: Version, - ) -> Result { + ) -> Result { // Start a handshake process. let local_pubkey = PublicKey::from(&local_privkey); let (mut h, local_eph_pubkey) = Handshake::new(local_privkey, protocol_version); @@ -339,7 +326,7 @@ impl SecretConnection { impl SecretConnection where IoHandler: TryClone, - ::Error: 'static + std::error::Error + Send + Sync, + ::Error: std::error::Error + Send + Sync + 'static, { /// For secret connections whose underlying I/O layer implements /// [`tendermint_std_ext::TryClone`], this attempts to split such a @@ -351,11 +338,14 @@ where /// ## Errors /// Fails when the `try_clone` operation for the underlying I/O handler /// fails. - pub fn split(self) -> Result<(Sender, Receiver)> { + pub fn split(self) -> Result<(Sender, Receiver), Error> { let remote_pubkey = self.remote_pubkey.expect("remote_pubkey to be initialized"); Ok(( Sender { - io_handler: self.io_handler.try_clone()?, + io_handler: self + .io_handler + .try_clone() + .map_err(|e| Error::transport_clone(e.to_string()))?, remote_pubkey, state: self.send_state, terminate: self.terminate.clone(), @@ -462,7 +452,7 @@ fn share_eph_pubkey( handler: &mut IoHandler, local_eph_pubkey: &EphemeralPublic, protocol_version: Version, -) -> Result { +) -> Result { // Send our pubkey and receive theirs in tandem. // TODO(ismail): on the go side this is done in parallel, here we do send and receive after // each other. thread::spawn would require a static lifetime. @@ -481,10 +471,8 @@ fn share_eph_pubkey( fn sign_challenge( challenge: &[u8; 32], local_privkey: &dyn Signer, -) -> Result { - local_privkey - .try_sign(challenge) - .map_err(|_| Error::Crypto.into()) +) -> Result { + local_privkey.try_sign(challenge).map_err(Error::signature) } // TODO(ismail): change from DecodeError to something more generic @@ -493,7 +481,7 @@ fn share_auth_signature( sc: &mut SecretConnection, pubkey: &ed25519::PublicKey, local_signature: &ed25519::Signature, -) -> Result { +) -> Result { let buf = sc .protocol_version .encode_auth_signature(pubkey, local_signature); @@ -522,7 +510,7 @@ fn encrypt( send_cipher: &ChaCha20Poly1305, send_nonce: &Nonce, sealed_frame: &mut [u8; TAG_SIZE + TOTAL_FRAME_SIZE], -) -> Result<()> { +) -> Result<(), Error> { assert!(!chunk.is_empty(), "chunk is empty"); assert!( chunk.len() <= TOTAL_FRAME_SIZE - DATA_LEN_SIZE, @@ -539,7 +527,7 @@ fn encrypt( b"", &mut sealed_frame[..TOTAL_FRAME_SIZE], ) - .map_err(|_| Error::Crypto)?; + .map_err(Error::aead)?; sealed_frame[TOTAL_FRAME_SIZE..].copy_from_slice(tag.as_slice()); @@ -584,21 +572,16 @@ fn decrypt( recv_cipher: &ChaCha20Poly1305, recv_nonce: &Nonce, out: &mut [u8], -) -> Result { +) -> Result { if ciphertext.len() < TAG_SIZE { - return Err(Error::Crypto).wrap_err_with(|| { - format!( - "ciphertext must be at least as long as a MAC tag {}", - TAG_SIZE - ) - }); + return Err(Error::short_ciphertext(TAG_SIZE)); } // Split ChaCha20 ciphertext from the Poly1305 tag let (ct, tag) = ciphertext.split_at(ciphertext.len() - TAG_SIZE); if out.len() < ct.len() { - return Err(Error::Crypto).wrap_err("output buffer is too small"); + return Err(Error::small_output_buffer()); } let in_out = &mut out[..ct.len()]; @@ -611,7 +594,7 @@ fn decrypt( in_out, tag.into(), ) - .map_err(|_| Error::Crypto)?; + .map_err(Error::aead)?; Ok(in_out.len()) } diff --git a/p2p/src/secret_connection/protocol.rs b/p2p/src/secret_connection/protocol.rs index 36c37122c..453051953 100644 --- a/p2p/src/secret_connection/protocol.rs +++ b/p2p/src/secret_connection/protocol.rs @@ -3,7 +3,6 @@ use std::convert::TryInto; use ed25519_dalek as ed25519; -use eyre::{Report, Result, WrapErr}; use prost::Message as _; #[cfg(feature = "amino")] @@ -81,14 +80,13 @@ impl Version { /// # Errors /// /// * if the message is malformed - pub fn decode_initial_handshake(self, bytes: &[u8]) -> Result { + pub fn decode_initial_handshake(self, bytes: &[u8]) -> Result { let eph_pubkey = if self.is_protobuf() { // Equivalent Go implementation: // https://github.com/tendermint/tendermint/blob/9e98c74/p2p/conn/secret_connection.go#L315-L323 // TODO(tarcieri): proper protobuf framing if bytes.len() != 34 || bytes[..2] != [0x0a, 0x20] { - return Err(Error::Protocol) - .wrap_err("malformed handshake message (protocol version mismatch?)"); + return Err(Error::malformed_handshake()); } let eph_pubkey_bytes: [u8; 32] = bytes[2..].try_into().expect("framing failed"); @@ -99,8 +97,7 @@ impl Version { // // Check that the length matches what we expect and the length prefix is correct if bytes.len() != 33 || bytes[0] != 32 { - return Err(Error::Protocol) - .wrap_err("malformed handshake message (protocol version mismatch?)"); + return Err(Error::malformed_handshake()); } let eph_pubkey_bytes: [u8; 32] = bytes[1..].try_into().expect("framing failed"); @@ -109,7 +106,7 @@ impl Version { // Reject the key if it is of low order if is_low_order_point(&eph_pubkey) { - return Err(Error::InvalidKey).wrap_err("low order key"); + return Err(Error::low_order_key()); } Ok(eph_pubkey) @@ -161,16 +158,10 @@ impl Version { /// # Errors /// /// * if the decoding of the bytes fails - pub fn decode_auth_signature(self, bytes: &[u8]) -> Result { + pub fn decode_auth_signature(self, bytes: &[u8]) -> Result { if self.is_protobuf() { // Parse Protobuf-encoded `AuthSigMessage` - proto::p2p::AuthSigMessage::decode_length_delimited(bytes).map_err(|e| { - let message = format!( - "malformed handshake message (protocol version mismatch?): {}", - e - ); - Report::new(Error::Protocol).wrap_err(message) - }) + proto::p2p::AuthSigMessage::decode_length_delimited(bytes).map_err(Error::decode) } else { self.decode_auth_signature_amino(bytes) } @@ -207,9 +198,13 @@ impl Version { #[allow(clippy::unused_self)] #[cfg(feature = "amino")] - fn decode_auth_signature_amino(self, bytes: &[u8]) -> Result { + fn decode_auth_signature_amino( + self, + bytes: &[u8], + ) -> Result { // Legacy Amino encoded `AuthSigMessage` - let amino_msg = amino_types::AuthSigMessage::decode_length_delimited(bytes)?; + let amino_msg = amino_types::AuthSigMessage::decode_length_delimited(bytes) + .map_err(Error::amino_decode)?; let pub_key = proto::crypto::PublicKey { sum: Some(proto::crypto::public_key::Sum::Ed25519(amino_msg.pub_key)), }; @@ -222,7 +217,7 @@ impl Version { #[allow(clippy::unused_self)] #[cfg(not(feature = "amino"))] - fn decode_auth_signature_amino(self, _: &[u8]) -> Result { + fn decode_auth_signature_amino(self, _: &[u8]) -> Result { panic!("attempted to decode auth signature using amino, but 'amino' feature is not present") } } diff --git a/p2p/src/secret_connection/public_key.rs b/p2p/src/secret_connection/public_key.rs index 5f83cfb2d..5d3b43d5e 100644 --- a/p2p/src/secret_connection/public_key.rs +++ b/p2p/src/secret_connection/public_key.rs @@ -3,10 +3,7 @@ use ed25519_dalek as ed25519; use sha2::{digest::Digest, Sha256}; use std::fmt::{self, Display}; -use tendermint::{ - error::{self, Error}, - node, -}; +use tendermint::{error::Error, node}; /// Secret Connection peer public keys (signing, presently Ed25519-only) #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -24,7 +21,7 @@ impl PublicKey { pub fn from_raw_ed25519(bytes: &[u8]) -> Result { ed25519::PublicKey::from_bytes(bytes) .map(Self::Ed25519) - .map_err(|_| error::Kind::Crypto.into()) + .map_err(Error::signature) } /// Get Ed25519 public key diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 36055d850..b41f165de 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -20,14 +20,20 @@ all-features = true prost = "0.7" prost-types = "0.7" bytes = "1.0" -anomaly = "0.2" -thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } subtle-encoding = "0.5" serde_bytes = "0.11" num-traits = "0.2" num-derive = "0.3" chrono = { version = "0.4", features = ["serde"] } +flex-error = { version = "0.4.1", default-features = false } [dev-dependencies] serde_json = "1.0" + +[features] +default = ["std", "eyre_tracer"] +eyre_tracer = ["flex-error/eyre_tracer"] +std = [ + "flex-error/std" +] diff --git a/proto/src/error.rs b/proto/src/error.rs index da6ec7970..ab2d30b27 100644 --- a/proto/src/error.rs +++ b/proto/src/error.rs @@ -1,37 +1,40 @@ //! This module defines the various errors that be raised during Protobuf conversions. -use anomaly::{BoxError, Context}; -use thiserror::Error; +use flex_error::{define_error, DisplayOnly}; +use prost::{DecodeError, EncodeError}; +use std::convert::TryFrom; +use std::fmt::Display; +use std::num::TryFromIntError; -/// An error that can be raised by the Protobuf conversions. -pub type Error = anomaly::Error; +define_error! { + Error { + TryFromProtobuf + { reason: String } + | e | { + format!("error converting message type into domain type: {}", + e.reason) + }, -/// Various kinds of errors that can be raised. -#[derive(Clone, Debug, Error)] -pub enum Kind { - /// TryFrom Prost Message failed during decoding - #[error("error converting message type into domain type")] - TryFromProtobuf, + EncodeMessage + [ DisplayOnly ] + | _ | { "error encoding message into buffer" }, - /// encoding prost Message into buffer failed - #[error("error encoding message into buffer")] - EncodeMessage, + DecodeMessage + [ DisplayOnly ] + | _ | { "error decoding buffer into message" }, - /// decoding buffer into prost Message failed - #[error("error decoding buffer into message")] - DecodeMessage, + ParseLength + [ DisplayOnly ] + | _ | { "error parsing encoded length" }, + } } -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())) +impl Error { + pub fn try_from(e: E) -> Error + where + E: Display, + T: TryFrom, + { + Error::try_from_protobuf(format!("{}", e)) } } diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 38d684ac6..2120c6305 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -15,17 +15,18 @@ pub mod google { } } +mod error; #[allow(warnings)] mod tendermint; + +pub use error::Error; pub use tendermint::*; -mod error; -use anomaly::BoxError; use bytes::{Buf, BufMut}; -pub use error::{Error, Kind}; use prost::encoding::encoded_len_varint; use prost::Message; use std::convert::{TryFrom, TryInto}; +use std::fmt::Display; pub mod serializers; @@ -109,7 +110,7 @@ pub mod serializers; pub trait Protobuf + Default> where Self: Sized + Clone + TryFrom, - >::Error: Into, + >::Error: Display, { /// Encode into a buffer in Protobuf format. /// @@ -120,7 +121,7 @@ where fn encode(&self, buf: &mut B) -> Result<(), Error> { T::from(self.clone()) .encode(buf) - .map_err(|e| Kind::EncodeMessage.context(e).into()) + .map_err(Error::encode_message) } /// Encode with a length-delimiter to a buffer in Protobuf format. @@ -134,7 +135,7 @@ where fn encode_length_delimited(&self, buf: &mut B) -> Result<(), Error> { T::from(self.clone()) .encode_length_delimited(buf) - .map_err(|e| Kind::EncodeMessage.context(e).into()) + .map_err(Error::encode_message) } /// Constructor that attempts to decode an instance from a buffer. @@ -146,10 +147,9 @@ where /// /// [`prost::Message::decode`]: https://docs.rs/prost/*/prost/trait.Message.html#method.decode fn decode(buf: B) -> Result { - T::decode(buf).map_or_else( - |e| Err(Kind::DecodeMessage.context(e).into()), - |t| Self::try_from(t).map_err(|e| Kind::TryFromProtobuf.context(e).into()), - ) + let raw = T::decode(buf).map_err(Error::decode_message)?; + + Self::try_from(raw).map_err(Error::try_from::) } /// Constructor that attempts to decode a length-delimited instance from @@ -162,10 +162,9 @@ where /// /// [`prost::Message::decode_length_delimited`]: https://docs.rs/prost/*/prost/trait.Message.html#method.decode_length_delimited fn decode_length_delimited(buf: B) -> Result { - T::decode_length_delimited(buf).map_or_else( - |e| Err(Kind::DecodeMessage.context(e).into()), - |t| Self::try_from(t).map_err(|e| Kind::TryFromProtobuf.context(e).into()), - ) + let raw = T::decode_length_delimited(buf).map_err(Error::decode_message)?; + + Self::try_from(raw).map_err(Error::try_from::) } /// Returns the encoded length of the message without a length delimiter. @@ -193,7 +192,7 @@ where /// Encode with a length-delimiter to a `Vec` Protobuf-encoded message. fn encode_length_delimited_vec(&self) -> Result, Error> { let len = self.encoded_len(); - let lenu64 = len.try_into().map_err(|e| Kind::EncodeMessage.context(e))?; + let lenu64 = len.try_into().map_err(Error::parse_length)?; let mut wire = Vec::with_capacity(len + encoded_len_varint(lenu64)); self.encode_length_delimited(&mut wire).map(|_| wire) } diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index e6c3dbce0..6fbc00e45 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -29,7 +29,8 @@ path = "src/client/bin/main.rs" required-features = [ "cli" ] [features] -default = [] +default = ["std", "eyre_tracer"] +eyre_tracer = ["flex-error/eyre_tracer"] cli = [ "http-client", "structopt", @@ -59,6 +60,9 @@ websocket-client = [ "tokio/time", "tracing" ] +std = [ + "flex-error/std" +] [dependencies] bytes = "1.0" @@ -75,7 +79,7 @@ uuid = { version = "0.8", default-features = false } subtle-encoding = { version = "0.5", features = ["bech32-preview"] } url = "2.2" walkdir = "2.3" - +flex-error = { version = "0.4.1", default-features = false } async-trait = { version = "0.1", optional = true } async-tungstenite = { version = "0.12", features = ["tokio-runtime", "tokio-rustls"], optional = true } futures = { version = "0.3", optional = true } diff --git a/rpc/src/client.rs b/rpc/src/client.rs index 1e61980ae..b5eebb7a6 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -14,10 +14,9 @@ pub use transport::websocket::{WebSocketClient, WebSocketClientDriver, WebSocket use crate::endpoint::validators::DEFAULT_VALIDATORS_PER_PAGE; use crate::endpoint::*; -use crate::error::Error; use crate::paging::Paging; use crate::query::Query; -use crate::{Order, Result, SimpleRequest}; +use crate::{Error, Order, SimpleRequest}; use async_trait::async_trait; use std::time::Duration; use tendermint::abci::{self, Transaction}; @@ -36,7 +35,7 @@ use tokio::time; #[async_trait] pub trait Client { /// `/abci_info`: get information about the ABCI application. - async fn abci_info(&self) -> Result { + async fn abci_info(&self) -> Result { Ok(self.perform(abci_info::Request).await?.response) } @@ -47,7 +46,7 @@ pub trait Client { data: V, height: Option, prove: bool, - ) -> Result + ) -> Result where V: Into> + Send, { @@ -58,7 +57,7 @@ pub trait Client { } /// `/block`: get block at a given height. - async fn block(&self, height: H) -> Result + async fn block(&self, height: H) -> Result where H: Into + Send, { @@ -66,12 +65,12 @@ pub trait Client { } /// `/block`: get the latest block. - async fn latest_block(&self) -> Result { + async fn latest_block(&self) -> Result { self.perform(block::Request::default()).await } /// `/block_results`: get ABCI results for a block at a particular height. - async fn block_results(&self, height: H) -> Result + async fn block_results(&self, height: H) -> Result where H: Into + Send, { @@ -80,7 +79,7 @@ pub trait Client { } /// `/block_results`: get ABCI results for the latest block. - async fn latest_block_results(&self) -> Result { + async fn latest_block_results(&self) -> Result { self.perform(block_results::Request::default()).await } @@ -89,7 +88,7 @@ pub trait Client { /// Block headers are returned in descending order (highest first). /// /// Returns at most 20 items. - async fn blockchain(&self, min: H, max: H) -> Result + async fn blockchain(&self, min: H, max: H) -> Result where H: Into + Send, { @@ -99,24 +98,33 @@ pub trait Client { } /// `/broadcast_tx_async`: broadcast a transaction, returning immediately. - async fn broadcast_tx_async(&self, tx: Transaction) -> Result { + async fn broadcast_tx_async( + &self, + tx: Transaction, + ) -> Result { self.perform(broadcast::tx_async::Request::new(tx)).await } /// `/broadcast_tx_sync`: broadcast a transaction, returning the response /// from `CheckTx`. - async fn broadcast_tx_sync(&self, tx: Transaction) -> Result { + async fn broadcast_tx_sync( + &self, + tx: Transaction, + ) -> Result { self.perform(broadcast::tx_sync::Request::new(tx)).await } /// `/broadcast_tx_commit`: broadcast a transaction, returning the response /// from `DeliverTx`. - async fn broadcast_tx_commit(&self, tx: Transaction) -> Result { + async fn broadcast_tx_commit( + &self, + tx: Transaction, + ) -> Result { self.perform(broadcast::tx_commit::Request::new(tx)).await } /// `/commit`: get block commit at a given height. - async fn commit(&self, height: H) -> Result + async fn commit(&self, height: H) -> Result where H: Into + Send, { @@ -124,13 +132,13 @@ pub trait Client { } /// `/consensus_state`: get current consensus state - async fn consensus_state(&self) -> Result { + async fn consensus_state(&self) -> Result { self.perform(consensus_state::Request::new()).await } // TODO(thane): Simplify once validators endpoint removes pagination. /// `/validators`: get validators a given height. - async fn validators(&self, height: H, paging: Paging) -> Result + async fn validators(&self, height: H, paging: Paging) -> Result where H: Into + Send, { @@ -178,41 +186,41 @@ pub trait Client { } /// `/commit`: get the latest block commit - async fn latest_commit(&self) -> Result { + async fn latest_commit(&self) -> Result { self.perform(commit::Request::default()).await } /// `/health`: get node health. /// /// Returns empty result (200 OK) on success, no response in case of an error. - async fn health(&self) -> Result<()> { + async fn health(&self) -> Result<(), Error> { self.perform(health::Request).await?; Ok(()) } /// `/genesis`: get genesis file. - async fn genesis(&self) -> Result { + async fn genesis(&self) -> Result { Ok(self.perform(genesis::Request).await?.genesis) } /// `/net_info`: obtain information about P2P and other network connections. - async fn net_info(&self) -> Result { + async fn net_info(&self) -> Result { self.perform(net_info::Request).await } /// `/status`: get Tendermint status including node info, pubkey, latest /// block hash, app hash, block height and time. - async fn status(&self) -> Result { + async fn status(&self) -> Result { self.perform(status::Request).await } /// `/broadcast_evidence`: broadcast an evidence. - async fn broadcast_evidence(&self, e: Evidence) -> Result { + async fn broadcast_evidence(&self, e: Evidence) -> Result { self.perform(evidence::Request::new(e)).await } /// `/tx`: find transaction by hash. - async fn tx(&self, hash: abci::transaction::Hash, prove: bool) -> Result { + async fn tx(&self, hash: abci::transaction::Hash, prove: bool) -> Result { self.perform(tx::Request::new(hash, prove)).await } @@ -224,14 +232,14 @@ pub trait Client { page: u32, per_page: u8, order: Order, - ) -> Result { + ) -> Result { self.perform(tx_search::Request::new(query, prove, page, per_page, order)) .await } /// Poll the `/health` endpoint until it returns a successful result or /// the given `timeout` has elapsed. - async fn wait_until_healthy(&self, timeout: T) -> Result<()> + async fn wait_until_healthy(&self, timeout: T) -> Result<(), Error> where T: Into + Send, { @@ -241,10 +249,7 @@ pub trait Client { while self.health().await.is_err() { if attempts_remaining == 0 { - return Err(Error::client_internal_error(format!( - "timed out waiting for healthy response after {}ms", - timeout.as_millis() - ))); + return Err(Error::timeout(timeout)); } attempts_remaining -= 1; @@ -255,7 +260,7 @@ pub trait Client { } /// Perform a request against the RPC endpoint - async fn perform(&self, request: R) -> Result + async fn perform(&self, request: R) -> Result where R: SimpleRequest; } diff --git a/rpc/src/client/bin/main.rs b/rpc/src/client/bin/main.rs index 058897ee0..950a9dfcd 100644 --- a/rpc/src/client/bin/main.rs +++ b/rpc/src/client/bin/main.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use structopt::StructOpt; use tendermint::abci::{Path, Transaction}; use tendermint_rpc::{ - Client, Error, HttpClient, Paging, Result, Scheme, SubscriptionClient, Url, WebSocketClient, + Client, Error, HttpClient, Paging, Scheme, SubscriptionClient, Url, WebSocketClient, }; use tracing::level_filters::LevelFilter; use tracing::{error, info, warn}; @@ -154,7 +154,7 @@ async fn main() { Scheme::Http | Scheme::Https => http_request(opt.url, proxy_url, opt.req).await, Scheme::WebSocket | Scheme::SecureWebSocket => match opt.proxy_url { Some(_) => Err(Error::invalid_params( - "proxies are only supported for use with HTTP clients at present", + "proxies are only supported for use with HTTP clients at present".to_string(), )), None => websocket_request(opt.url, opt.req).await, }, @@ -169,7 +169,7 @@ async fn main() { // 1. If supplied, that's the proxy URL used. // 2. If not supplied, but environment variable HTTP_PROXY or HTTPS_PROXY are // supplied, then use the appropriate variable for the URL in question. -fn get_http_proxy_url(url_scheme: Scheme, proxy_url: Option) -> Result> { +fn get_http_proxy_url(url_scheme: Scheme, proxy_url: Option) -> Result, Error> { match proxy_url { Some(u) => Ok(Some(u)), None => match url_scheme { @@ -191,7 +191,7 @@ fn get_http_proxy_url(url_scheme: Scheme, proxy_url: Option) -> Result, req: Request) -> Result<()> { +async fn http_request(url: Url, proxy_url: Option, req: Request) -> Result<(), Error> { let client = match proxy_url { Some(proxy_url) => { info!( @@ -211,7 +211,7 @@ async fn http_request(url: Url, proxy_url: Option, req: Request) -> Result< } } -async fn websocket_request(url: Url, req: Request) -> Result<()> { +async fn websocket_request(url: Url, req: Request) -> Result<(), Error> { info!("Using WebSocket client to submit request to: {}", url); let (client, driver) = WebSocketClient::new(url).await?; let driver_hdl = tokio::spawn(async move { driver.run().await }); @@ -221,18 +221,18 @@ async fn websocket_request(url: Url, req: Request) -> Result<()> { }; client.close()?; - driver_hdl - .await - .map_err(|e| Error::client_internal_error(e.to_string()))??; + driver_hdl.await.map_err(Error::join)??; result } -async fn client_request(client: &C, req: ClientRequest) -> Result<()> +async fn client_request(client: &C, req: ClientRequest) -> Result<(), Error> where C: Client + Sync, { let result = match req { - ClientRequest::AbciInfo => serde_json::to_string_pretty(&client.abci_info().await?)?, + ClientRequest::AbciInfo => { + serde_json::to_string_pretty(&client.abci_info().await?).map_err(Error::serde)? + } ClientRequest::AbciQuery { path, data, @@ -241,54 +241,73 @@ where } => serde_json::to_string_pretty( &client .abci_query( - path.map(|s| Path::from_str(&s)).transpose()?, + path.map(|s| Path::from_str(&s)) + .transpose() + .map_err(Error::tendermint)?, data, height.map(Into::into), prove, ) .await?, - )?, + ) + .map_err(Error::serde)?, ClientRequest::Block { height } => { - serde_json::to_string_pretty(&client.block(height).await?)? + serde_json::to_string_pretty(&client.block(height).await?).map_err(Error::serde)? } ClientRequest::Blockchain { min, max } => { - serde_json::to_string_pretty(&client.blockchain(min, max).await?)? + serde_json::to_string_pretty(&client.blockchain(min, max).await?) + .map_err(Error::serde)? } ClientRequest::BlockResults { height } => { - serde_json::to_string_pretty(&client.block_results(height).await?)? + serde_json::to_string_pretty(&client.block_results(height).await?) + .map_err(Error::serde)? } ClientRequest::BroadcastTxAsync { tx } => serde_json::to_string_pretty( &client .broadcast_tx_async(Transaction::from(tx.into_bytes())) .await?, - )?, + ) + .map_err(Error::serde)?, ClientRequest::BroadcastTxCommit { tx } => serde_json::to_string_pretty( &client .broadcast_tx_commit(Transaction::from(tx.into_bytes())) .await?, - )?, + ) + .map_err(Error::serde)?, ClientRequest::BroadcastTxSync { tx } => serde_json::to_string_pretty( &client .broadcast_tx_sync(Transaction::from(tx.into_bytes())) .await?, - )?, + ) + .map_err(Error::serde)?, ClientRequest::Commit { height } => { - serde_json::to_string_pretty(&client.commit(height).await?)? + serde_json::to_string_pretty(&client.commit(height).await?).map_err(Error::serde)? + } + ClientRequest::LatestBlock => { + serde_json::to_string_pretty(&client.latest_block().await?).map_err(Error::serde)? } - ClientRequest::LatestBlock => serde_json::to_string_pretty(&client.latest_block().await?)?, ClientRequest::LatestBlockResults => { - serde_json::to_string_pretty(&client.latest_block_results().await?)? + serde_json::to_string_pretty(&client.latest_block_results().await?) + .map_err(Error::serde)? } ClientRequest::LatestCommit => { - serde_json::to_string_pretty(&client.latest_commit().await?)? + serde_json::to_string_pretty(&client.latest_commit().await?).map_err(Error::serde)? } ClientRequest::ConsensusState => { - serde_json::to_string_pretty(&client.consensus_state().await?)? + serde_json::to_string_pretty(&client.consensus_state().await?).map_err(Error::serde)? + } + ClientRequest::Genesis => { + serde_json::to_string_pretty(&client.genesis().await?).map_err(Error::serde)? + } + ClientRequest::Health => { + serde_json::to_string_pretty(&client.health().await?).map_err(Error::serde)? + } + ClientRequest::NetInfo => { + serde_json::to_string_pretty(&client.net_info().await?).map_err(Error::serde)? + } + ClientRequest::Status => { + serde_json::to_string_pretty(&client.status().await?).map_err(Error::serde)? } - ClientRequest::Genesis => serde_json::to_string_pretty(&client.genesis().await?)?, - ClientRequest::Health => serde_json::to_string_pretty(&client.health().await?)?, - ClientRequest::NetInfo => serde_json::to_string_pretty(&client.net_info().await?)?, - ClientRequest::Status => serde_json::to_string_pretty(&client.status().await?)?, ClientRequest::Validators { height, all, @@ -306,9 +325,11 @@ where None => Paging::Default, } }; - serde_json::to_string_pretty(&client.validators(height, paging).await?)? + serde_json::to_string_pretty(&client.validators(height, paging).await?) + .map_err(Error::serde)? } }; + println!("{}", result); Ok(()) } diff --git a/rpc/src/client/subscription.rs b/rpc/src/client/subscription.rs index 4d69d601d..8b913dcc5 100644 --- a/rpc/src/client/subscription.rs +++ b/rpc/src/client/subscription.rs @@ -3,7 +3,7 @@ use crate::client::sync::{ChannelRx, ChannelTx}; use crate::event::Event; use crate::query::Query; -use crate::Result; +use crate::Error; use async_trait::async_trait; use futures::task::{Context, Poll}; use futures::Stream; @@ -15,7 +15,7 @@ use std::pin::Pin; #[async_trait] pub trait SubscriptionClient { /// `/subscribe`: subscribe to receive events produced by the given query. - async fn subscribe(&self, query: Query) -> Result; + async fn subscribe(&self, query: Query) -> Result; /// `/unsubscribe`: unsubscribe from events relating to the given query. /// @@ -26,15 +26,15 @@ pub trait SubscriptionClient { /// terminate them separately. /// /// [`select_all`]: https://docs.rs/futures/*/futures/stream/fn.select_all.html - async fn unsubscribe(&self, query: Query) -> Result<()>; + async fn unsubscribe(&self, query: Query) -> Result<(), Error>; /// Subscription clients will usually have long-running underlying /// transports that will need to be closed at some point. - fn close(self) -> Result<()>; + fn close(self) -> Result<(), Error>; } -pub(crate) type SubscriptionTx = ChannelTx>; -pub(crate) type SubscriptionRx = ChannelRx>; +pub(crate) type SubscriptionTx = ChannelTx>; +pub(crate) type SubscriptionRx = ChannelRx>; /// An interface that can be used to asynchronously receive [`Event`]s for a /// particular subscription. @@ -74,7 +74,7 @@ pub struct Subscription { } impl Stream for Subscription { - type Item = Result; + type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.project().rx.poll_next(cx) diff --git a/rpc/src/client/sync.rs b/rpc/src/client/sync.rs index 47900a32d..466d8977a 100644 --- a/rpc/src/client/sync.rs +++ b/rpc/src/client/sync.rs @@ -11,7 +11,7 @@ use futures::Stream; use pin_project::pin_project; use tokio::sync::mpsc; -use crate::{Error, Result}; +use crate::Error; /// Constructor for an unbounded channel. pub fn unbounded() -> (ChannelTx, ChannelRx) { @@ -27,13 +27,8 @@ pub fn unbounded() -> (ChannelTx, ChannelRx) { pub struct ChannelTx(mpsc::UnboundedSender); impl ChannelTx { - pub fn send(&self, value: T) -> Result<()> { - self.0.send(value).map_err(|e| { - Error::client_internal_error(format!( - "failed to send message to internal channel: {}", - e - )) - }) + pub fn send(&self, value: T) -> Result<(), Error> { + self.0.send(value).map_err(Error::send) } } diff --git a/rpc/src/client/transport/http.rs b/rpc/src/client/transport/http.rs index 4513b3b0b..40abb925c 100644 --- a/rpc/src/client/transport/http.rs +++ b/rpc/src/client/transport/http.rs @@ -1,7 +1,7 @@ //! HTTP-based transport for Tendermint RPC Client. use crate::client::Client; -use crate::{Error, Result, Scheme, SimpleRequest, Url}; +use crate::{Error, Scheme, SimpleRequest, Url}; use async_trait::async_trait; use std::convert::{TryFrom, TryInto}; use std::str::FromStr; @@ -41,7 +41,7 @@ pub struct HttpClient { impl HttpClient { /// Construct a new Tendermint RPC HTTP/S client connecting to the given /// URL. - pub fn new(url: U) -> Result + pub fn new(url: U) -> Result where U: TryInto, { @@ -62,7 +62,7 @@ impl HttpClient { /// attempt to connect using the [HTTP CONNECT] method. /// /// [HTTP CONNECT]: https://en.wikipedia.org/wiki/HTTP_tunnel - pub fn new_with_proxy(url: U, proxy_url: P) -> Result + pub fn new_with_proxy(url: U, proxy_url: P) -> Result where U: TryInto, P: TryInto, @@ -81,7 +81,7 @@ impl HttpClient { #[async_trait] impl Client for HttpClient { - async fn perform(&self, request: R) -> Result + async fn perform(&self, request: R) -> Result where R: SimpleRequest, { @@ -98,13 +98,10 @@ pub struct HttpClientUrl(Url); impl TryFrom for HttpClientUrl { type Error = Error; - fn try_from(value: Url) -> Result { + fn try_from(value: Url) -> Result { match value.scheme() { Scheme::Http | Scheme::Https => Ok(Self(value)), - _ => Err(Error::invalid_params(&format!( - "cannot use URL {} with HTTP clients", - value - ))), + _ => Err(Error::invalid_url(value)), } } } @@ -112,7 +109,7 @@ impl TryFrom for HttpClientUrl { impl FromStr for HttpClientUrl { type Err = Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { let url: Url = s.parse()?; url.try_into() } @@ -121,7 +118,7 @@ impl FromStr for HttpClientUrl { impl TryFrom<&str> for HttpClientUrl { type Error = Error; - fn try_from(value: &str) -> Result { + fn try_from(value: &str) -> Result { value.parse() } } @@ -129,16 +126,14 @@ impl TryFrom<&str> for HttpClientUrl { impl TryFrom for HttpClientUrl { type Error = Error; - fn try_from(value: net::Address) -> Result { + fn try_from(value: net::Address) -> Result { match value { net::Address::Tcp { peer_id: _, host, port, } => format!("http://{}:{}", host, port).parse(), - net::Address::Unix { .. } => Err(Error::invalid_params( - "only TCP-based node addresses are supported", - )), + net::Address::Unix { .. } => Err(Error::invalid_network_address()), } } } @@ -152,13 +147,13 @@ impl From for Url { impl TryFrom for hyper::Uri { type Error = Error; - fn try_from(value: HttpClientUrl) -> Result { - Ok(value.0.to_string().parse()?) + fn try_from(value: HttpClientUrl) -> Result { + value.0.to_string().parse().map_err(Error::invalid_uri) } } mod sealed { - use crate::{Error, Response, Result, SimpleRequest}; + use crate::{Error, Response, SimpleRequest}; use hyper::body::Buf; use hyper::client::connect::Connect; use hyper::client::HttpConnector; @@ -184,12 +179,12 @@ mod sealed { where C: Connect + Clone + Send + Sync + 'static, { - pub async fn perform(&self, request: R) -> Result + pub async fn perform(&self, request: R) -> Result where R: SimpleRequest, { let request = self.build_request(request)?; - let response = self.inner.request(request).await?; + let response = self.inner.request(request).await.map_err(Error::hyper)?; let response_body = response_to_string(response).await?; tracing::debug!("Incoming response: {}", response_body); R::Response::from_string(&response_body) @@ -201,13 +196,14 @@ mod sealed { pub fn build_request( &self, request: R, - ) -> Result> { + ) -> Result, Error> { let request_body = request.into_json(); let mut request = hyper::Request::builder() .method("POST") .uri(&self.uri) - .body(hyper::Body::from(request_body.into_bytes()))?; + .body(hyper::Body::from(request_body.into_bytes())) + .map_err(Error::http)?; { let headers = request.headers_mut(); @@ -249,26 +245,29 @@ mod sealed { )) } - pub fn new_http_proxy(uri: Uri, proxy_uri: Uri) -> Result { + pub fn new_http_proxy(uri: Uri, proxy_uri: Uri) -> Result { let proxy = Proxy::new(Intercept::All, proxy_uri); - let proxy_connector = ProxyConnector::from_proxy(HttpConnector::new(), proxy)?; + let proxy_connector = + ProxyConnector::from_proxy(HttpConnector::new(), proxy).map_err(Error::io)?; Ok(Self::HttpProxy(HyperClient::new( uri, hyper::Client::builder().build(proxy_connector), ))) } - pub fn new_https_proxy(uri: Uri, proxy_uri: Uri) -> Result { + pub fn new_https_proxy(uri: Uri, proxy_uri: Uri) -> Result { let proxy = Proxy::new(Intercept::All, proxy_uri); let proxy_connector = - ProxyConnector::from_proxy(HttpsConnector::with_native_roots(), proxy)?; + ProxyConnector::from_proxy(HttpsConnector::with_native_roots(), proxy) + .map_err(Error::io)?; + Ok(Self::HttpsProxy(HyperClient::new( uri, hyper::Client::builder().build(proxy_connector), ))) } - pub async fn perform(&self, request: R) -> Result + pub async fn perform(&self, request: R) -> Result where R: SimpleRequest, { @@ -281,13 +280,15 @@ mod sealed { } } - async fn response_to_string(response: hyper::Response) -> Result { + async fn response_to_string(response: hyper::Response) -> Result { let mut response_body = String::new(); hyper::body::aggregate(response.into_body()) - .await? + .await + .map_err(Error::hyper)? .reader() .read_to_string(&mut response_body) - .map_err(|_| Error::client_internal_error("failed to read response body to string"))?; + .map_err(Error::io)?; + Ok(response_body) } } diff --git a/rpc/src/client/transport/mock.rs b/rpc/src/client/transport/mock.rs index 327f97df3..1efcabc85 100644 --- a/rpc/src/client/transport/mock.rs +++ b/rpc/src/client/transport/mock.rs @@ -6,7 +6,7 @@ use crate::client::transport::router::SubscriptionRouter; use crate::event::Event; use crate::query::Query; use crate::utils::uuid_str; -use crate::{Client, Error, Method, Request, Response, Result, Subscription, SubscriptionClient}; +use crate::{Client, Error, Method, Request, Response, Subscription, SubscriptionClient}; use async_trait::async_trait; use std::collections::HashMap; @@ -54,13 +54,13 @@ pub struct MockClient { #[async_trait] impl Client for MockClient { - async fn perform(&self, request: R) -> Result + async fn perform(&self, request: R) -> Result where R: Request, { - self.matcher.response_for(request).ok_or_else(|| { - Error::client_internal_error("no matching response for incoming request") - })? + self.matcher + .response_for(request) + .ok_or_else(Error::mismatch_response)? } } @@ -90,7 +90,7 @@ impl MockClient { #[async_trait] impl SubscriptionClient for MockClient { - async fn subscribe(&self, query: Query) -> Result { + async fn subscribe(&self, query: Query) -> Result { let id = uuid_str(); let (subs_tx, subs_rx) = unbounded(); let (result_tx, mut result_rx) = unbounded(); @@ -104,14 +104,14 @@ impl SubscriptionClient for MockClient { Ok(Subscription::new(id, query, subs_rx)) } - async fn unsubscribe(&self, query: Query) -> Result<()> { + async fn unsubscribe(&self, query: Query) -> Result<(), Error> { let (result_tx, mut result_rx) = unbounded(); self.driver_tx .send(DriverCommand::Unsubscribe { query, result_tx })?; result_rx.recv().await.unwrap() } - fn close(self) -> Result<()> { + fn close(self) -> Result<(), Error> { Ok(()) } } @@ -122,11 +122,11 @@ pub enum DriverCommand { id: String, query: Query, subscription_tx: SubscriptionTx, - result_tx: ChannelTx>, + result_tx: ChannelTx>, }, Unsubscribe { query: Query, - result_tx: ChannelTx>, + result_tx: ChannelTx>, }, Publish(Box), Terminate, @@ -146,7 +146,7 @@ impl MockClientDriver { } } - pub async fn run(mut self) -> Result<()> { + pub async fn run(mut self) -> Result<(), Error> { loop { tokio::select! { Some(cmd) = self.rx.recv() => match cmd { @@ -168,13 +168,13 @@ impl MockClientDriver { id: String, query: Query, subscription_tx: SubscriptionTx, - result_tx: ChannelTx>, + result_tx: ChannelTx>, ) { self.router.add(id, query, subscription_tx); result_tx.send(Ok(())).unwrap(); } - fn unsubscribe(&mut self, query: Query, result_tx: ChannelTx>) { + fn unsubscribe(&mut self, query: Query, result_tx: ChannelTx>) { self.router.remove_by_query(query); result_tx.send(Ok(())).unwrap(); } @@ -190,7 +190,7 @@ impl MockClientDriver { /// [`MockClient`]: struct.MockClient.html pub trait MockRequestMatcher: Send + Sync { /// Provide the corresponding response for the given request (if any). - fn response_for(&self, request: R) -> Option> + fn response_for(&self, request: R) -> Option> where R: Request; } @@ -201,11 +201,11 @@ pub trait MockRequestMatcher: Send + Sync { /// [`MockRequestMatcher`]: trait.MockRequestMatcher.html #[derive(Debug)] pub struct MockRequestMethodMatcher { - mappings: HashMap>, + mappings: HashMap>, } impl MockRequestMatcher for MockRequestMethodMatcher { - fn response_for(&self, request: R) -> Option> + fn response_for(&self, request: R) -> Option> where R: Request, { @@ -230,7 +230,7 @@ impl MockRequestMethodMatcher { /// /// Successful responses must be JSON-encoded. #[allow(dead_code)] - pub fn map(mut self, method: Method, response: Result) -> Self { + pub fn map(mut self, method: Method, response: Result) -> Self { self.mappings.insert(method, response); self } @@ -301,8 +301,8 @@ mod test { } // Here each subscription's channel is drained. - let subs1_events = subs1_events.collect::>>().await; - let subs2_events = subs2_events.collect::>>().await; + let subs1_events = subs1_events.collect::>>().await; + let subs2_events = subs2_events.collect::>>().await; assert_eq!(3, subs1_events.len()); assert_eq!(3, subs2_events.len()); diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index 078aaa4d1..3a4f865d4 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -8,7 +8,7 @@ use crate::event::Event; use crate::query::Query; use crate::request::Wrapper; use crate::{ - response, Client, Error, Id, Request, Response, Result, Scheme, SimpleRequest, Subscription, + error::Error, response, Client, Id, Request, Response, Scheme, SimpleRequest, Subscription, SubscriptionClient, Url, }; use async_trait::async_trait; @@ -132,7 +132,7 @@ impl WebSocketClient { /// Tendermint node's RPC endpoint. /// /// Supports both `ws://` and `wss://` protocols. - pub async fn new(url: U) -> Result<(Self, WebSocketClientDriver)> + pub async fn new(url: U) -> Result<(Self, WebSocketClientDriver), Error> where U: TryInto, { @@ -148,7 +148,7 @@ impl WebSocketClient { #[async_trait] impl Client for WebSocketClient { - async fn perform(&self, request: R) -> Result<::Response> + async fn perform(&self, request: R) -> Result<::Response, Error> where R: SimpleRequest, { @@ -158,15 +158,15 @@ impl Client for WebSocketClient { #[async_trait] impl SubscriptionClient for WebSocketClient { - async fn subscribe(&self, query: Query) -> Result { + async fn subscribe(&self, query: Query) -> Result { self.inner.subscribe(query).await } - async fn unsubscribe(&self, query: Query) -> Result<()> { + async fn unsubscribe(&self, query: Query) -> Result<(), Error> { self.inner.unsubscribe(query).await } - fn close(self) -> Result<()> { + fn close(self) -> Result<(), Error> { self.inner.close() } } @@ -180,10 +180,10 @@ pub struct WebSocketClientUrl(Url); impl TryFrom for WebSocketClientUrl { type Error = Error; - fn try_from(value: Url) -> Result { + fn try_from(value: Url) -> Result { match value.scheme() { Scheme::WebSocket | Scheme::SecureWebSocket => Ok(Self(value)), - _ => Err(Error::invalid_params(&format!( + _ => Err(Error::invalid_params(format!( "cannot use URL {} with WebSocket clients", value ))), @@ -194,7 +194,7 @@ impl TryFrom for WebSocketClientUrl { impl FromStr for WebSocketClientUrl { type Err = Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { let url: Url = s.parse()?; url.try_into() } @@ -203,7 +203,7 @@ impl FromStr for WebSocketClientUrl { impl TryFrom<&str> for WebSocketClientUrl { type Error = Error; - fn try_from(value: &str) -> Result { + fn try_from(value: &str) -> Result { value.parse() } } @@ -211,7 +211,7 @@ impl TryFrom<&str> for WebSocketClientUrl { impl TryFrom for WebSocketClientUrl { type Error = Error; - fn try_from(value: net::Address) -> Result { + fn try_from(value: net::Address) -> Result { match value { net::Address::Tcp { peer_id: _, @@ -219,7 +219,7 @@ impl TryFrom for WebSocketClientUrl { port, } => format!("ws://{}:{}/websocket", host, port).parse(), net::Address::Unix { .. } => Err(Error::invalid_params( - "only TCP-based node addresses are supported", + "only TCP-based node addresses are supported".to_string(), )), } } @@ -240,7 +240,7 @@ mod sealed { use crate::query::Query; use crate::request::Wrapper; use crate::utils::uuid_str; - use crate::{Error, Response, Result, SimpleRequest, Subscription, Url}; + use crate::{Error, Response, SimpleRequest, Subscription, Url}; use async_tungstenite::tokio::{connect_async, connect_async_with_tls_connector}; use tracing::debug; @@ -275,10 +275,10 @@ mod sealed { /// this driver becomes the responsibility of the client owner, and must be /// executed in a separate asynchronous context to the client to ensure it /// doesn't block the client. - pub async fn new(url: Url) -> Result<(Self, WebSocketClientDriver)> { + pub async fn new(url: Url) -> Result<(Self, WebSocketClientDriver), Error> { let url = url.to_string(); debug!("Connecting to unsecure WebSocket endpoint: {}", url); - let (stream, _response) = connect_async(url).await?; + let (stream, _response) = connect_async(url).await.map_err(Error::tungstenite)?; let (cmd_tx, cmd_rx) = unbounded(); let driver = WebSocketClientDriver::new(stream, cmd_rx); Ok(( @@ -301,12 +301,14 @@ mod sealed { /// this driver becomes the responsibility of the client owner, and must be /// executed in a separate asynchronous context to the client to ensure it /// doesn't block the client. - pub async fn new(url: Url) -> Result<(Self, WebSocketClientDriver)> { + pub async fn new(url: Url) -> Result<(Self, WebSocketClientDriver), Error> { let url = url.to_string(); debug!("Connecting to secure WebSocket endpoint: {}", url); // Not supplying a connector means async_tungstenite will create the // connector for us. - let (stream, _response) = connect_async_with_tls_connector(url, None).await?; + let (stream, _response) = connect_async_with_tls_connector(url, None) + .await + .map_err(Error::tungstenite)?; let (cmd_tx, cmd_rx) = unbounded(); let driver = WebSocketClientDriver::new(stream, cmd_rx); Ok(( @@ -320,16 +322,11 @@ mod sealed { } impl AsyncTungsteniteClient { - fn send_cmd(&self, cmd: DriverCommand) -> Result<()> { - self.cmd_tx.send(cmd).map_err(|e| { - Error::client_internal_error(format!( - "failed to send command to client driver: {}", - e - )) - }) + fn send_cmd(&self, cmd: DriverCommand) -> Result<(), Error> { + self.cmd_tx.send(cmd) } - pub async fn perform(&self, request: R) -> Result + pub async fn perform(&self, request: R) -> Result where R: SimpleRequest, { @@ -343,15 +340,13 @@ mod sealed { response_tx, }))?; let response = response_rx.recv().await.ok_or_else(|| { - Error::client_internal_error( - "failed to hear back from WebSocket driver".to_string(), - ) + Error::client_internal("failed to hear back from WebSocket driver".to_string()) })??; tracing::debug!("Incoming response: {}", response); R::Response::from_string(response) } - pub async fn subscribe(&self, query: Query) -> Result { + pub async fn subscribe(&self, query: Query) -> Result { let (subscription_tx, subscription_rx) = unbounded(); let (response_tx, mut response_rx) = unbounded(); // By default we use UUIDs to differentiate subscriptions @@ -364,29 +359,25 @@ mod sealed { }))?; // Make sure our subscription request went through successfully. let _ = response_rx.recv().await.ok_or_else(|| { - Error::client_internal_error( - "failed to hear back from WebSocket driver".to_string(), - ) + Error::client_internal("failed to hear back from WebSocket driver".to_string()) })??; Ok(Subscription::new(id, query, subscription_rx)) } - pub async fn unsubscribe(&self, query: Query) -> Result<()> { + pub async fn unsubscribe(&self, query: Query) -> Result<(), Error> { let (response_tx, mut response_rx) = unbounded(); self.send_cmd(DriverCommand::Unsubscribe(UnsubscribeCommand { query: query.to_string(), response_tx, }))?; let _ = response_rx.recv().await.ok_or_else(|| { - Error::client_internal_error( - "failed to hear back from WebSocket driver".to_string(), - ) + Error::client_internal("failed to hear back from WebSocket driver".to_string()) })??; Ok(()) } /// Signals to the driver that it must terminate. - pub fn close(self) -> Result<()> { + pub fn close(self) -> Result<(), Error> { self.send_cmd(DriverCommand::Terminate) } } @@ -400,17 +391,17 @@ mod sealed { } impl WebSocketClient { - pub async fn new_unsecure(url: Url) -> Result<(Self, WebSocketClientDriver)> { + pub async fn new_unsecure(url: Url) -> Result<(Self, WebSocketClientDriver), Error> { let (client, driver) = AsyncTungsteniteClient::::new(url).await?; Ok((Self::Unsecure(client), driver)) } - pub async fn new_secure(url: Url) -> Result<(Self, WebSocketClientDriver)> { + pub async fn new_secure(url: Url) -> Result<(Self, WebSocketClientDriver), Error> { let (client, driver) = AsyncTungsteniteClient::::new(url).await?; Ok((Self::Secure(client), driver)) } - pub async fn perform(&self, request: R) -> Result + pub async fn perform(&self, request: R) -> Result where R: SimpleRequest, { @@ -420,21 +411,21 @@ mod sealed { } } - pub async fn subscribe(&self, query: Query) -> Result { + pub async fn subscribe(&self, query: Query) -> Result { match self { WebSocketClient::Unsecure(c) => c.subscribe(query).await, WebSocketClient::Secure(c) => c.subscribe(query).await, } } - pub async fn unsubscribe(&self, query: Query) -> Result<()> { + pub async fn unsubscribe(&self, query: Query) -> Result<(), Error> { match self { WebSocketClient::Unsecure(c) => c.unsubscribe(query).await, WebSocketClient::Secure(c) => c.unsubscribe(query).await, } } - pub fn close(self) -> Result<()> { + pub fn close(self) -> Result<(), Error> { match self { WebSocketClient::Unsecure(c) => c.close(), WebSocketClient::Secure(c) => c.close(), @@ -465,7 +456,7 @@ struct SubscribeCommand { // Where to send subscription events. subscription_tx: SubscriptionTx, // Where to send the result of the subscription request. - response_tx: ChannelTx>, + response_tx: ChannelTx>, } #[derive(Debug, Clone)] @@ -473,7 +464,7 @@ struct UnsubscribeCommand { // The query from which to unsubscribe. query: String, // Where to send the result of the unsubscribe request. - response_tx: ChannelTx>, + response_tx: ChannelTx>, } #[derive(Debug, Clone)] @@ -485,7 +476,7 @@ struct SimpleRequestCommand { // The wrapped and serialized JSON-RPC request. wrapped_request: String, // Where to send the result of the simple request. - response_tx: ChannelTx>, + response_tx: ChannelTx>, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -521,7 +512,7 @@ impl WebSocketClientDriver { /// Executes the WebSocket driver, which manages the underlying WebSocket /// transport. - pub async fn run(mut self) -> Result<()> { + pub async fn run(mut self) -> Result<(), Error> { let mut ping_interval = tokio::time::interval_at(Instant::now().add(PING_INTERVAL), PING_INTERVAL); @@ -538,8 +529,9 @@ impl WebSocketClientDriver { self.handle_incoming_msg(msg).await? }, Err(e) => return Err( - Error::websocket_error( - format!("failed to read from WebSocket connection: {}", e), + Error::web_socket( + "failed to read from WebSocket connection".to_string(), + e ), ), }, @@ -551,22 +543,19 @@ impl WebSocketClientDriver { }, _ = ping_interval.tick() => self.ping().await?, _ = &mut recv_timeout => { - return Err(Error::websocket_error(format!( - "reading from WebSocket connection timed out after {} seconds", - RECV_TIMEOUT.as_secs() - ))); + return Err(Error::web_socket_timeout(RECV_TIMEOUT)); } } } } - async fn send_msg(&mut self, msg: Message) -> Result<()> { + async fn send_msg(&mut self, msg: Message) -> Result<(), Error> { self.stream.send(msg).await.map_err(|e| { - Error::websocket_error(format!("failed to write to WebSocket connection: {}", e)) + Error::web_socket("failed to write to WebSocket connection".to_string(), e) }) } - async fn send_request(&mut self, wrapper: Wrapper) -> Result<()> + async fn send_request(&mut self, wrapper: Wrapper) -> Result<(), Error> where R: Request, { @@ -576,7 +565,7 @@ impl WebSocketClientDriver { .await } - async fn subscribe(&mut self, cmd: SubscribeCommand) -> Result<()> { + async fn subscribe(&mut self, cmd: SubscribeCommand) -> Result<(), Error> { // If we already have an active subscription for the given query, // there's no need to initiate another one. Just add this subscription // to the router. @@ -601,7 +590,7 @@ impl WebSocketClientDriver { Ok(()) } - async fn unsubscribe(&mut self, cmd: UnsubscribeCommand) -> Result<()> { + async fn unsubscribe(&mut self, cmd: UnsubscribeCommand) -> Result<(), Error> { // Terminate all subscriptions for this query immediately. This // prioritizes acknowledgement of the caller's wishes over networking // problems. @@ -625,7 +614,7 @@ impl WebSocketClientDriver { Ok(()) } - async fn simple_request(&mut self, cmd: SimpleRequestCommand) -> Result<()> { + async fn simple_request(&mut self, cmd: SimpleRequestCommand) -> Result<(), Error> { if let Err(e) = self .send_msg(Message::Text(cmd.wrapped_request.clone())) .await @@ -638,7 +627,7 @@ impl WebSocketClientDriver { Ok(()) } - async fn handle_incoming_msg(&mut self, msg: Message) -> Result<()> { + async fn handle_incoming_msg(&mut self, msg: Message) -> Result<(), Error> { match msg { Message::Text(s) => self.handle_text_msg(s).await, Message::Ping(v) => self.pong(v).await, @@ -646,7 +635,7 @@ impl WebSocketClientDriver { } } - async fn handle_text_msg(&mut self, msg: String) -> Result<()> { + async fn handle_text_msg(&mut self, msg: String) -> Result<(), Error> { if let Ok(ev) = Event::from_string(&msg) { self.publish_event(ev).await; return Ok(()); @@ -726,7 +715,7 @@ impl WebSocketClientDriver { &mut self, pending_cmd: DriverCommand, response: String, - ) -> Result<()> { + ) -> Result<(), Error> { match pending_cmd { DriverCommand::Subscribe(cmd) => { let (id, query, subscription_tx, response_tx) = @@ -740,15 +729,15 @@ impl WebSocketClientDriver { } } - async fn pong(&mut self, v: Vec) -> Result<()> { + async fn pong(&mut self, v: Vec) -> Result<(), Error> { self.send_msg(Message::Pong(v)).await } - async fn ping(&mut self) -> Result<()> { + async fn ping(&mut self) -> Result<(), Error> { self.send_msg(Message::Ping(Vec::new())).await } - async fn close(mut self) -> Result<()> { + async fn close(mut self) -> Result<(), Error> { self.send_msg(Message::Close(Some(CloseFrame { code: CloseCode::Normal, reason: Cow::from("client closed WebSocket connection"), @@ -783,8 +772,8 @@ mod test { // Interface to a driver that manages all incoming WebSocket connections. struct TestServer { node_addr: net::Address, - driver_hdl: JoinHandle>, - terminate_tx: ChannelTx>, + driver_hdl: JoinHandle>, + terminate_tx: ChannelTx>, event_tx: ChannelTx, } @@ -809,11 +798,11 @@ mod test { } } - fn publish_event(&mut self, ev: Event) -> Result<()> { + fn publish_event(&mut self, ev: Event) -> Result<(), Error> { self.event_tx.send(ev) } - async fn terminate(self) -> Result<()> { + async fn terminate(self) -> Result<(), Error> { self.terminate_tx.send(Ok(())).unwrap(); self.driver_hdl.await.unwrap() } @@ -823,7 +812,7 @@ mod test { struct TestServerDriver { listener: TcpListener, event_rx: ChannelRx, - terminate_rx: ChannelRx>, + terminate_rx: ChannelRx>, handlers: Vec, } @@ -831,7 +820,7 @@ mod test { fn new( listener: TcpListener, event_rx: ChannelRx, - terminate_rx: ChannelRx>, + terminate_rx: ChannelRx>, ) -> Self { Self { listener, @@ -841,7 +830,7 @@ mod test { } } - async fn run(mut self) -> Result<()> { + async fn run(mut self) -> Result<(), Error> { loop { tokio::select! { Some(ev) = self.event_rx.recv() => self.publish_event(ev), @@ -883,8 +872,8 @@ mod test { // Interface to a driver that manages a single incoming WebSocket // connection. struct TestServerHandler { - driver_hdl: JoinHandle>, - terminate_tx: ChannelTx>, + driver_hdl: JoinHandle>, + terminate_tx: ChannelTx>, event_tx: ChannelTx, } @@ -907,7 +896,7 @@ mod test { let _ = self.event_tx.send(ev); } - async fn terminate(self) -> Result<()> { + async fn terminate(self) -> Result<(), Error> { self.terminate_tx.send(Ok(()))?; self.driver_hdl.await.unwrap() } @@ -917,7 +906,7 @@ mod test { struct TestServerHandlerDriver { conn: WebSocketStream>, event_rx: ChannelRx, - terminate_rx: ChannelRx>, + terminate_rx: ChannelRx>, // A mapping of subscription queries to subscription IDs for this // connection. subscriptions: HashMap, @@ -927,7 +916,7 @@ mod test { fn new( conn: WebSocketStream>, event_rx: ChannelRx, - terminate_rx: ChannelRx>, + terminate_rx: ChannelRx>, ) -> Self { Self { conn, @@ -937,7 +926,7 @@ mod test { } } - async fn run(mut self) -> Result<()> { + async fn run(mut self) -> Result<(), Error> { loop { tokio::select! { Some(msg) = self.conn.next() => { @@ -962,7 +951,7 @@ mod test { let _ = self.send(Id::Str(subs_id), ev).await; } - async fn handle_incoming_msg(&mut self, msg: Message) -> Option> { + async fn handle_incoming_msg(&mut self, msg: Message) -> Option> { match msg { Message::Text(s) => self.handle_incoming_text_msg(s).await, Message::Ping(v) => { @@ -977,7 +966,7 @@ mod test { } } - async fn handle_incoming_text_msg(&mut self, msg: String) -> Option> { + async fn handle_incoming_text_msg(&mut self, msg: String) -> Option> { match serde_json::from_str::(&msg) { Ok(json_msg) => { if let Some(json_method) = json_msg.get("method") { diff --git a/rpc/src/endpoint/consensus_state.rs b/rpc/src/endpoint/consensus_state.rs index 663393e30..38569585d 100644 --- a/rpc/src/endpoint/consensus_state.rs +++ b/rpc/src/endpoint/consensus_state.rs @@ -181,92 +181,92 @@ impl FromStr for VoteSummary { let parts: Vec<&str> = s .strip_prefix("Vote{") .ok_or_else(|| { - Self::Err::client_internal_error( - "invalid format for consensus state vote summary string", + Error::client_internal( + "invalid format for consensus state vote summary string".to_string(), ) })? .strip_suffix('}') .ok_or_else(|| { - Self::Err::client_internal_error( - "invalid format for consensus state vote summary string", + Error::client_internal( + "invalid format for consensus state vote summary string".to_string(), ) })? .split(' ') .collect(); if parts.len() != 6 { - return Err(Self::Err::client_internal_error(format!( + return Err(Error::client_internal(format!( "expected 6 parts to a consensus state vote summary, but got {}", parts.len() ))); } let validator: Vec<&str> = parts[0].split(':').collect(); if validator.len() != 2 { - return Err(Self::Err::client_internal_error(format!( + return Err(Error::client_internal(format!( "failed to parse validator info for consensus state vote summary: {}", parts[0], ))); } let height_round_type: Vec<&str> = parts[1].split('/').collect(); if height_round_type.len() != 3 { - return Err(Self::Err::client_internal_error(format!( + return Err(Error::client_internal(format!( "failed to parse height/round/type for consensus state vote summary: {}", parts[1] ))); } let validator_index = i32::from_str(validator[0]).map_err(|e| { - Self::Err::client_internal_error(format!( + Error::client_internal(format!( "failed to parse validator index from consensus state vote summary: {} ({})", e, validator[0], )) })?; let validator_address_fingerprint = Fingerprint::from_str(validator[1]).map_err(|e| { - Self::Err::client_internal_error(format!( + Error::client_internal(format!( "failed to parse validator address fingerprint from consensus state vote summary: {}", e )) })?; let height = Height::from_str(height_round_type[0]).map_err(|e| { - Self::Err::client_internal_error(format!( + Error::client_internal(format!( "failed to parse height from consensus state vote summary: {}", e )) })?; let round = Round::from_str(height_round_type[1]).map_err(|e| { - Self::Err::client_internal_error(format!( + Error::client_internal(format!( "failed to parse round from consensus state vote summary: {}", e )) })?; let vote_type_parts: Vec<&str> = height_round_type[2].split('(').collect(); if vote_type_parts.len() != 2 { - return Err(Self::Err::client_internal_error(format!( + return Err(Error::client_internal(format!( "invalid structure for vote type in consensus state vote summary: {}", height_round_type[2] ))); } let vote_type_str = vote_type_parts[1].trim_end_matches(')'); let vote_type = vote::Type::from_str(vote_type_str).map_err(|e| { - Self::Err::client_internal_error(format!( + Error::client_internal(format!( "failed to parse vote type from consensus state vote summary: {} ({})", e, vote_type_str )) })?; let block_id_hash_fingerprint = Fingerprint::from_str(parts[2]).map_err(|e| { - Self::Err::client_internal_error(format!( + Error::client_internal(format!( "failed to parse block ID hash fingerprint from consensus state vote summary: {}", e )) })?; let signature_fingerprint = Fingerprint::from_str(parts[3]).map_err(|e| { - Self::Err::client_internal_error(format!( + Error::client_internal(format!( "failed to parse signature fingerprint from consensus state vote summary: {}", e )) })?; let timestamp = Time::parse_from_rfc3339(parts[5]).map_err(|e| { - Self::Err::client_internal_error(format!( + Error::client_internal(format!( "failed to parse timestamp from consensus state vote summary: {}", e )) @@ -311,7 +311,7 @@ impl FromStr for Fingerprint { fn from_str(s: &str) -> Result { Ok(Self(hex::decode_upper(s).map_err(|e| { - Self::Err::client_internal_error(format!( + Error::client_internal(format!( "failed to parse fingerprint as an uppercase hexadecimal string: {}", e )) diff --git a/rpc/src/error.rs b/rpc/src/error.rs index 7a9596ac4..0ba405e1a 100644 --- a/rpc/src/error.rs +++ b/rpc/src/error.rs @@ -1,290 +1,223 @@ //! JSON-RPC error types -#[cfg(feature = "websocket-client")] -use async_tungstenite::tungstenite::Error as WSError; - -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt::{self, Display}; -use thiserror::Error; - -// TODO(thane): Differentiate between RPC response errors and internal crate -// errors (e.g. domain type-related errors). -/// Tendermint RPC errors -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct Error { - /// Error code - code: Code, - - /// Error message - message: String, - - /// Additional data about the error - data: Option, -} -impl std::error::Error for Error {} - +use flex_error::{define_error, DefaultTracer, DisplayError, DisplayOnly, ErrorMessageTracer}; +use std::time::Duration; + +use crate::response_error::ResponseError; +use crate::rpc_url::Url; + +#[cfg(feature = "http")] +type HttpError = flex_error::DisplayOnly; + +#[cfg(not(feature = "http"))] +type HttpError = flex_error::NoSource; + +#[cfg(feature = "http")] +type InvalidUriError = flex_error::DisplayOnly; + +#[cfg(not(feature = "http"))] +type InvalidUriError = flex_error::NoSource; + +#[cfg(feature = "hyper")] +type HyperError = flex_error::DisplayOnly; + +#[cfg(not(feature = "hyper"))] +type HyperError = flex_error::NoSource; + +#[cfg(feature = "tokio")] +type JoinError = flex_error::DisplayOnly; + +#[cfg(not(feature = "tokio"))] +type JoinError = flex_error::NoSource; + +#[cfg(feature = "async-tungstenite")] +type TungsteniteError = flex_error::DisplayOnly; + +#[cfg(not(feature = "async-tungstenite"))] +type TungsteniteError = flex_error::NoSource; + +define_error! { + #[derive(Debug, Clone)] + Error { + Response + [ DisplayError ] + | _ | { "response error" }, + + Io + [ DisplayOnly ] + | _ | { "I/O error" }, + + Http + [ HttpError ] + | _ | { "HTTP error" }, + + Hyper + [ HyperError ] + | _ | { "HTTP error" }, + + InvalidParams + { + message: String + } + | e | { + format_args!("invalid params error: {}", e.message) + }, + + WebSocket + { + message: String + } + [ TungsteniteError ] + | e | { + format_args!("web socket error: {}", e.message) + }, + + WebSocketTimeout + { + timeout: Duration + } + | e | { + format_args!("reading from WebSocket connection timed out after {} seconds", + e.timeout.as_secs()) + }, + + MethodNotFound + { + method: String + } + | e | { + format_args!("method not found: {}", e.method) + }, + + Parse + { + reason: String + } + | e | { + format_args!("parse error: {}", e.reason) + }, + + Server + { + reason: String + } + | e | { + format_args!("server error: {}", e.reason) + }, + + ClientInternal + { + reason: String + } + | e | { + format_args!("client internal error: {}", e.reason) + }, + + Timeout + { + duration: Duration + } + | e | { + format_args!( + "timed out waiting for healthy response after {}ms", + e.duration.as_millis() + ) + }, + + ChannelSend + | _ | { "failed to send message to internal channel" }, + + InvalidUrl + { url: Url } + | e | { + format_args!( + "cannot use URL {} with HTTP clients", + e.url + ) + }, + + InvalidUri + [ InvalidUriError ] + | _ | { "invalid URI" }, + + Tendermint + [ tendermint::Error ] + | _ | { "tendermint error" }, + + ParseInt + [ DisplayOnly ] + | _ | { "error parsing integer" }, + + OutOfRange + [ DisplayOnly ] + | _ | { "number out of range" }, + + InvalidNetworkAddress + | _ | { "only TCP-based node addresses are supported" }, + + MismatchResponse + | _ | { "no matching response for incoming request" }, + + UnrecognizedEventType + { + event_type: String + } + | e | { + format_args!("unrecognized event type: {}", e.event_type) + }, + + Serde + [ DisplayOnly ] + | _ | { "serde parse error" }, + + ParseUrl + [ DisplayOnly ] + | _ | { "parse error" }, + + Tungstenite + [ TungsteniteError ] + | _ | { "tungstenite error" }, + + Join + [ JoinError ] + | _ | { "join error" }, + + MalformedJson + | _ | { "server returned malformatted JSON (no 'result' or 'error')" }, + + UnsupportedScheme + { + scheme: String + } + | e | { + format_args!("unsupported scheme: {}", e.scheme) + }, + + UnsupportedRpcVersion + { + version: String, + supported: String + } + | e | { + format_args!("server RPC version unsupported: '{}' (only '{}' supported)", + e.version, e.supported) + }, + + } +} + +impl Clone for Error { + fn clone(&self) -> Self { + Error( + self.detail().clone(), + DefaultTracer::new_message(self.trace()), + ) + } +} + +#[cfg(feature = "tokio")] impl Error { - /// Create a new RPC error - pub fn new(code: Code, data: Option) -> Error { - let message = code.to_string(); - - Error { - code, - message, - data, - } - } - - /// Create a low-level HTTP error - pub fn http_error(message: impl Into) -> Error { - Error { - code: Code::HttpError, - message: message.into(), - data: None, - } - } - - /// Create a new invalid parameter error - pub fn invalid_params(data: &str) -> Error { - Error::new(Code::InvalidParams, Some(data.to_string())) - } - - /// Create a new websocket error - pub fn websocket_error(cause: impl Into) -> Error { - Error::new(Code::WebSocketError, Some(cause.into())) - } - - /// Create a new method-not-found error - pub fn method_not_found(name: &str) -> Error { - Error::new(Code::MethodNotFound, Some(name.to_string())) - } - - /// Create a new parse error - pub fn parse_error(error: E) -> Error - where - E: Display, - { - Error::new(Code::ParseError, Some(error.to_string())) - } - - /// Create a new server error - pub fn server_error(data: D) -> Error - where - D: Display, - { - Error::new(Code::ServerError, Some(data.to_string())) - } - - /// An internal error occurred within the client. - pub fn client_internal_error(cause: impl Into) -> Error { - Error::new(Code::ClientInternalError, Some(cause.into())) - } - - /// Obtain the `rpc::error::Code` for this error - pub fn code(&self) -> Code { - self.code - } - - /// Borrow the error message (if available) - pub fn message(&self) -> &str { - &self.message - } - - /// Optional additional error message (if available) - pub fn data(&self) -> Option<&str> { - self.data.as_ref().map(AsRef::as_ref) - } -} - -impl Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.data { - Some(data) => write!( - f, - "{}: {} (code: {})", - self.message, - data, - self.code.value() - ), - None => write!(f, "{} (code: {})", self.message, self.code.value()), - } - } -} - -impl From for Error { - fn from(e: std::io::Error) -> Self { - Error::client_internal_error(e.to_string()) - } -} - -impl From for Error { - fn from(e: url::ParseError) -> Self { - Error::invalid_params(&e.to_string()) - } -} - -#[cfg(feature = "http-client")] -impl From for Error { - fn from(http_error: http::Error) -> Error { - Error::http_error(http_error.to_string()) - } -} - -#[cfg(feature = "http-client")] -impl From for Error { - fn from(hyper_error: hyper::Error) -> Error { - Error::http_error(hyper_error.to_string()) - } -} - -#[cfg(feature = "http-client")] -impl From for Error { - fn from(e: http::uri::InvalidUri) -> Self { - Error::http_error(e.to_string()) - } -} - -#[cfg(feature = "websocket-client")] -impl From for Error { - fn from(websocket_error: WSError) -> Error { - Error::websocket_error(websocket_error.to_string()) - } -} - -#[cfg(feature = "cli")] -impl From for Error { - fn from(e: serde_json::Error) -> Self { - Error::client_internal_error(e.to_string()) - } -} - -#[cfg(feature = "cli")] -impl From for Error { - fn from(e: tendermint::Error) -> Self { - Error::client_internal_error(e.to_string()) - } -} - -/// Tendermint RPC error codes. -/// -/// See `func RPC*Error()` definitions in: -/// -#[derive(Copy, Clone, Debug, Eq, Error, Hash, PartialEq, PartialOrd, Ord)] -pub enum Code { - /// Low-level HTTP error - #[error("HTTP error")] - HttpError, - - /// Low-level WebSocket error - #[error("WebSocket Error")] - WebSocketError, - - /// An internal error occurred within the client. - /// - /// This is an error unique to this client, and is not available in the - /// [Go client]. - /// - /// [Go client]: https://github.com/tendermint/tendermint/tree/master/rpc/jsonrpc/client - #[error("Client internal error")] - ClientInternalError, - - /// Parse error i.e. invalid JSON (-32700) - #[error("Parse error. Invalid JSON")] - ParseError, - - /// Invalid request (-32600) - #[error("Invalid Request")] - InvalidRequest, - - /// Method not found error (-32601) - #[error("Method not found")] - MethodNotFound, - - /// Invalid parameters (-32602) - #[error("Invalid params")] - InvalidParams, - - /// Internal RPC server error (-32603) - #[error("Internal error")] - InternalError, - - /// Server error (-32000) - #[error("Server error")] - ServerError, - - /// Other error types - #[error("Error (code: {})", 0)] - Other(i32), -} - -impl Code { - /// Get the integer error value for this code - pub fn value(self) -> i32 { - i32::from(self) - } -} - -impl From for Code { - fn from(value: i32) -> Code { - match value { - 0 => Code::HttpError, - 1 => Code::WebSocketError, - 2 => Code::ClientInternalError, - -32700 => Code::ParseError, - -32600 => Code::InvalidRequest, - -32601 => Code::MethodNotFound, - -32602 => Code::InvalidParams, - -32603 => Code::InternalError, - -32000 => Code::ServerError, - other => Code::Other(other), - } - } -} - -impl From for i32 { - fn from(code: Code) -> i32 { - match code { - Code::HttpError => 0, - Code::WebSocketError => 1, - Code::ClientInternalError => 2, - Code::ParseError => -32700, - Code::InvalidRequest => -32600, - Code::MethodNotFound => -32601, - Code::InvalidParams => -32602, - Code::InternalError => -32603, - Code::ServerError => -32000, - Code::Other(other) => other, - } - } -} - -impl<'de> Deserialize<'de> for Code { - fn deserialize>(deserializer: D) -> Result { - Ok(Code::from(i32::deserialize(deserializer)?)) - } -} - -impl Serialize for Code { - fn serialize(&self, serializer: S) -> Result { - self.value().serialize(serializer) - } -} - -#[cfg(test)] -mod tests { - use super::Code; - use super::Error; - - #[test] - fn test_serialize() { - let expect = - "{\"code\":-32700,\"message\":\"Parse error. Invalid JSON\",\"data\":\"hello world\"}"; - let pe = Error::parse_error("hello world"); - let pe_json = serde_json::to_string(&pe).expect("could not write JSON"); - assert_eq!(pe_json, expect); - let res: Error = serde_json::from_str(expect).expect("could not read JSON"); - assert_eq!(res.code, Code::ParseError); - assert_eq!(res.code.value(), -32700); - assert_eq!(res.data, Some("hello world".to_string())); + pub fn send(_: tokio::sync::mpsc::error::SendError) -> Error { + Error::channel_send() } } diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index b629778f0..b55977c96 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -8,14 +8,12 @@ //! //! Several client-related features are provided at present: //! -//! * `http-client` - Provides [`HttpClient`], which is a basic RPC client that -//! interacts with remote Tendermint nodes via **JSON-RPC over HTTP or -//! HTTPS**. This client does not provide [`event::Event`] subscription -//! functionality. See the [Tendermint RPC] for more details. -//! * `websocket-client` - Provides [`WebSocketClient`], which provides full -//! client functionality, including general RPC functionality as well as -//! [`event::Event`] subscription functionality. Can be used over secure -//! (`wss://`) and unsecure (`ws://`) connections. +//! * `http-client` - Provides [`HttpClient`], which is a basic RPC client that interacts with +//! remote Tendermint nodes via **JSON-RPC over HTTP or HTTPS**. This client does not provide +//! [`event::Event`] subscription functionality. See the [Tendermint RPC] for more details. +//! * `websocket-client` - Provides [`WebSocketClient`], which provides full client functionality, +//! including general RPC functionality as well as [`event::Event`] subscription functionality. +//! Can be used over secure (`wss://`) and unsecure (`ws://`) connections. //! //! ### Mock Clients //! @@ -50,7 +48,7 @@ mod paging; pub mod query; pub mod request; pub mod response; -mod result; +pub mod response_error; mod rpc_url; mod utils; mod version; @@ -62,6 +60,6 @@ pub use order::Order; pub use paging::{PageNumber, Paging, PerPage}; pub use request::{Request, SimpleRequest}; pub use response::Response; -pub use result::Result; +pub use response_error::{Code, ResponseError}; pub use rpc_url::{Scheme, Url}; pub use version::Version; diff --git a/rpc/src/method.rs b/rpc/src/method.rs index bc44eb72e..2a964184b 100644 --- a/rpc/src/method.rs +++ b/rpc/src/method.rs @@ -1,6 +1,6 @@ //! JSON-RPC request methods -use super::Error; +use crate::Error; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ fmt::{self, Display}, @@ -126,7 +126,7 @@ impl FromStr for Method { "tx_search" => Method::TxSearch, "unsubscribe" => Method::Unsubscribe, "validators" => Method::Validators, - other => return Err(Error::method_not_found(other)), + other => return Err(Error::method_not_found(other.to_string())), }) } } diff --git a/rpc/src/order.rs b/rpc/src/order.rs index 79ff77142..f5079cc60 100644 --- a/rpc/src/order.rs +++ b/rpc/src/order.rs @@ -23,7 +23,7 @@ impl FromStr for Order { match s { "asc" => Ok(Self::Ascending), "desc" => Ok(Self::Descending), - _ => Err(Error::invalid_params(&format!( + _ => Err(Error::invalid_params(format!( "invalid order type: {} (must be \"asc\" or \"desc\")", s ))), diff --git a/rpc/src/paging.rs b/rpc/src/paging.rs index 19f538d1d..25768dcca 100644 --- a/rpc/src/paging.rs +++ b/rpc/src/paging.rs @@ -29,10 +29,8 @@ impl FromStr for PageNumber { type Err = Error; fn from_str(s: &str) -> Result { - let raw = i64::from_str(s).map_err(|e| Error::client_internal_error(e.to_string()))?; - let raw_usize: usize = raw.try_into().map_err(|_| { - Error::client_internal_error(format!("page number out of range: {}", raw)) - })?; + let raw = i64::from_str(s).map_err(Error::parse_int)?; + let raw_usize: usize = raw.try_into().map_err(Error::out_of_range)?; Ok(raw_usize.into()) } } @@ -57,10 +55,8 @@ impl FromStr for PerPage { type Err = Error; fn from_str(s: &str) -> Result { - let raw = i64::from_str(s).map_err(|e| Error::client_internal_error(e.to_string()))?; - let raw_u8: u8 = raw.try_into().map_err(|_| { - Error::client_internal_error(format!("items per page out of range: {}", raw)) - })?; + let raw = i64::from_str(s).map_err(Error::parse_int)?; + let raw_u8: u8 = raw.try_into().map_err(Error::out_of_range)?; Ok(raw_u8.into()) } } diff --git a/rpc/src/query.rs b/rpc/src/query.rs index 892e3e148..ca82f265a 100644 --- a/rpc/src/query.rs +++ b/rpc/src/query.rs @@ -7,7 +7,7 @@ // TODO(thane): These warnings are generated by the PEG for some reason. Try to fix and remove. #![allow(clippy::redundant_closure_call, clippy::unit_arg)] -use crate::{Error, Result}; +use crate::Error; use chrono::{Date, DateTime, FixedOffset, Utc}; use std::fmt; use std::str::FromStr; @@ -228,14 +228,11 @@ impl fmt::Display for EventType { impl FromStr for EventType { type Err = Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { match s { "NewBlock" => Ok(Self::NewBlock), "Tx" => Ok(Self::Tx), - invalid => Err(Error::invalid_params(&format!( - "unrecognized event type: {}", - invalid - ))), + invalid => Err(Error::unrecognized_event_type(invalid.to_string())), } } } diff --git a/rpc/src/response.rs b/rpc/src/response.rs index 53e172f5d..454fce22e 100644 --- a/rpc/src/response.rs +++ b/rpc/src/response.rs @@ -1,6 +1,7 @@ //! JSON-RPC response types -use super::{Error, Id, Version}; +use crate::response_error::ResponseError; +use crate::{Error, Id, Version}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::io::Read; @@ -9,13 +10,13 @@ pub trait Response: Serialize + DeserializeOwned + Sized { /// Parse a JSON-RPC response from a JSON string fn from_string(response: impl AsRef<[u8]>) -> Result { let wrapper: Wrapper = - serde_json::from_slice(response.as_ref()).map_err(Error::parse_error)?; + serde_json::from_slice(response.as_ref()).map_err(Error::serde)?; wrapper.into_result() } /// Parse a JSON-RPC response from an `io::Reader` fn from_reader(reader: impl Read) -> Result { - let wrapper: Wrapper = serde_json::from_reader(reader).map_err(Error::parse_error)?; + let wrapper: Wrapper = serde_json::from_reader(reader).map_err(Error::serde)?; wrapper.into_result() } } @@ -33,7 +34,7 @@ pub struct Wrapper { result: Option, /// Error message if unsuccessful - error: Option, + error: Option, } impl Wrapper @@ -53,7 +54,7 @@ where /// Convert this wrapper into the underlying error, if any pub fn into_error(self) -> Option { - self.error + self.error.map(Error::response) } /// Convert this wrapper into a result type @@ -61,19 +62,17 @@ where // Ensure we're using a supported RPC version self.version().ensure_supported()?; - if let Some(error) = self.error { - Err(error) + if let Some(e) = self.error { + Err(Error::response(e)) } else if let Some(result) = self.result { Ok(result) } else { - Err(Error::server_error( - "server returned malformatted JSON (no 'result' or 'error')", - )) + Err(Error::malformed_json()) } } #[cfg(test)] - pub fn new_with_id(id: Id, result: Option, error: Option) -> Self { + pub fn new_with_id(id: Id, result: Option, error: Option) -> Self { Self { jsonrpc: Version::current(), id, diff --git a/rpc/src/response_error.rs b/rpc/src/response_error.rs new file mode 100644 index 000000000..087218a83 --- /dev/null +++ b/rpc/src/response_error.rs @@ -0,0 +1,207 @@ +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ResponseError { + /// Error code + code: Code, + + /// Error message + message: String, + + /// Additional data about the error + data: Option, +} + +// /// Tendermint RPC error codes. +// /// +/// See `func RPC*Error()` definitions in: +/// +#[derive(Copy, Clone, Debug, Eq, thiserror::Error, Hash, PartialEq, PartialOrd, Ord)] +pub enum Code { + /// Low-level HTTP error + #[error("HTTP error")] + HttpError, + + /// Low-level WebSocket error + #[error("WebSocket Error")] + WebSocketError, + + /// An internal error occurred within the client. + /// + /// This is an error unique to this client, and is not available in the + /// [Go client]. + /// + /// [Go client]: https://github.com/tendermint/tendermint/tree/master/rpc/jsonrpc/client + #[error("Client internal error")] + ClientInternalError, + + /// Parse error i.e. invalid JSON (-32700) + #[error("Parse error. Invalid JSON")] + ParseError, + + /// Invalid request (-32600) + #[error("Invalid Request")] + InvalidRequest, + + /// Method not found error (-32601) + #[error("Method not found")] + MethodNotFound, + + /// Invalid parameters (-32602) + #[error("Invalid params")] + InvalidParams, + + /// Internal RPC server error (-32603) + #[error("Internal error")] + InternalError, + + /// Server error (-32000) + #[error("Server error")] + ServerError, + + /// Other error types + #[error("Error (code: {})", 0)] + Other(i32), +} + +impl Code { + /// Get the integer error value for this code + pub fn value(self) -> i32 { + i32::from(self) + } +} + +impl From for Code { + fn from(value: i32) -> Code { + match value { + 0 => Code::HttpError, + 1 => Code::WebSocketError, + 2 => Code::ClientInternalError, + -32700 => Code::ParseError, + -32600 => Code::InvalidRequest, + -32601 => Code::MethodNotFound, + -32602 => Code::InvalidParams, + -32603 => Code::InternalError, + -32000 => Code::ServerError, + other => Code::Other(other), + } + } +} + +impl From for i32 { + fn from(code: Code) -> i32 { + match code { + Code::HttpError => 0, + Code::WebSocketError => 1, + Code::ClientInternalError => 2, + Code::ParseError => -32700, + Code::InvalidRequest => -32600, + Code::MethodNotFound => -32601, + Code::InvalidParams => -32602, + Code::InternalError => -32603, + Code::ServerError => -32000, + Code::Other(other) => other, + } + } +} + +impl Display for ResponseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.data { + Some(data) => write!( + f, + "{}: {} (code: {})", + self.message, + data, + self.code.value() + ), + None => write!(f, "{} (code: {})", self.message, self.code.value()), + } + } +} + +impl ResponseError { + /// Create a new RPC error + pub fn new(code: Code, data: Option) -> ResponseError { + let message = code.to_string(); + + ResponseError { + code, + message, + data, + } + } + + // / Create a low-level HTTP error + pub fn http_error(message: impl Into) -> ResponseError { + ResponseError { + code: Code::HttpError, + message: message.into(), + data: None, + } + } + + /// Create a new invalid parameter error + pub fn invalid_params(data: &str) -> ResponseError { + ResponseError::new(Code::InvalidParams, Some(data.to_string())) + } + + /// Create a new websocket error + pub fn websocket_error(cause: impl Into) -> ResponseError { + ResponseError::new(Code::WebSocketError, Some(cause.into())) + } + + /// Create a new method-not-found error + pub fn method_not_found(name: &str) -> ResponseError { + ResponseError::new(Code::MethodNotFound, Some(name.to_string())) + } + + /// Create a new parse error + pub fn parse_error(error: E) -> ResponseError + where + E: Display, + { + ResponseError::new(Code::ParseError, Some(error.to_string())) + } + + /// Create a new server error + pub fn server_error(data: D) -> ResponseError + where + D: Display, + { + ResponseError::new(Code::ServerError, Some(data.to_string())) + } + + /// An internal error occurred within the client. + pub fn client_internal_error(cause: impl Into) -> ResponseError { + ResponseError::new(Code::ClientInternalError, Some(cause.into())) + } + + /// Obtain the `rpc::error::Code` for this error + pub fn code(&self) -> Code { + self.code + } + + /// Borrow the error message (if available) + pub fn message(&self) -> &str { + &self.message + } + + /// Optional additional error message (if available) + pub fn data(&self) -> Option<&str> { + self.data.as_ref().map(AsRef::as_ref) + } +} + +impl<'de> Deserialize<'de> for Code { + fn deserialize>(deserializer: D) -> Result { + Ok(Code::from(i32::deserialize(deserializer)?)) + } +} + +impl Serialize for Code { + fn serialize(&self, serializer: S) -> Result { + self.value().serialize(serializer) + } +} diff --git a/rpc/src/result.rs b/rpc/src/result.rs deleted file mode 100644 index 48bf5873e..000000000 --- a/rpc/src/result.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::Error; - -/// RPC client-related result type alias. -pub type Result = std::result::Result; diff --git a/rpc/src/rpc_url.rs b/rpc/src/rpc_url.rs index c0896df68..3b077a629 100644 --- a/rpc/src/rpc_url.rs +++ b/rpc/src/rpc_url.rs @@ -1,6 +1,7 @@ //! URL representation for RPC clients. -use serde::de::Error; +use crate::error::Error; +use serde::de::Error as SerdeError; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryFrom; use std::fmt; @@ -35,12 +36,7 @@ impl FromStr for Scheme { "https" => Scheme::Https, "ws" => Scheme::WebSocket, "wss" => Scheme::SecureWebSocket, - _ => { - return Err(crate::Error::invalid_params(&format!( - "unsupported scheme: {}", - s - ))) - } + _ => return Err(Error::unsupported_scheme(s.to_string())), }) } } @@ -59,22 +55,20 @@ pub struct Url { } impl FromStr for Url { - type Err = crate::Error; + type Err = Error; fn from_str(s: &str) -> Result { - let inner: url::Url = s.parse()?; + let inner: url::Url = s.parse().map_err(Error::parse_url)?; + let scheme: Scheme = inner.scheme().parse()?; + let host = inner .host_str() - .ok_or_else(|| { - crate::Error::invalid_params(&format!("URL is missing its host: {}", s)) - })? + .ok_or_else(|| Error::invalid_params(format!("URL is missing its host: {}", s)))? .to_owned(); + let port = inner.port_or_known_default().ok_or_else(|| { - crate::Error::invalid_params(&format!( - "cannot determine appropriate port for URL: {}", - s - )) + Error::invalid_params(format!("cannot determine appropriate port for URL: {}", s)) })?; Ok(Self { inner, diff --git a/rpc/src/version.rs b/rpc/src/version.rs index 79c310d66..6722ae2be 100644 --- a/rpc/src/version.rs +++ b/rpc/src/version.rs @@ -31,10 +31,10 @@ impl Version { if self.is_supported() { Ok(()) } else { - Err(Error::server_error(&format!( - "server RPC version unsupported: '{}' (only '{}' supported)", - self.0, SUPPORTED_VERSION - ))) + Err(Error::unsupported_rpc_version( + self.0.to_string(), + SUPPORTED_VERSION.to_string(), + )) } } } diff --git a/rpc/tests/kvstore_fixtures.rs b/rpc/tests/kvstore_fixtures.rs index 3a761dec8..47954e268 100644 --- a/rpc/tests/kvstore_fixtures.rs +++ b/rpc/tests/kvstore_fixtures.rs @@ -3,7 +3,12 @@ use std::str::FromStr; use std::{fs, path::PathBuf}; use subtle_encoding::{base64, hex}; -use tendermint_rpc::{endpoint, error::Code, request::Wrapper as RequestWrapper, Order, Response}; +use tendermint_rpc::{ + endpoint, + error::{Error, ErrorDetail}, + request::Wrapper as RequestWrapper, + Code, Order, Response, +}; use walkdir::WalkDir; const CHAIN_ID: &str = "dockerchain"; @@ -306,15 +311,20 @@ fn incoming_fixtures() { assert!(result.response.value.is_empty()); } "block_at_height_0" => { - let error = endpoint::block::Response::from_string(&content) - .err() - .unwrap(); - assert_eq!(error.code(), Code::InternalError); - assert_eq!(error.message(), "Internal error"); - assert_eq!( - error.data(), - Some("height must be greater than 0, but got 0") - ); + let res = endpoint::block::Response::from_string(&content); + + match res { + Err(Error(ErrorDetail::Response(e), _)) => { + let response = e.source; + assert_eq!(response.code(), Code::InternalError); + assert_eq!(response.message(), "Internal error"); + assert_eq!( + response.data(), + Some("height must be greater than 0, but got 0") + ); + } + _ => panic!("expected Response error"), + } } "block_at_height_1" => { let result = endpoint::block::Response::from_string(content).unwrap(); @@ -724,23 +734,26 @@ fn incoming_fixtures() { assert_eq!(result.validator_info.power.value(), 10); } "subscribe_malformed" => { - let result = endpoint::subscribe::Response::from_string(content) - .err() - .unwrap(); - assert_eq!(result.code(), Code::InternalError); - assert_eq!(result.message(), "Internal error"); - assert_eq!(result.data().unwrap(),"failed to parse query: \nparse error near PegText (line 1 symbol 2 - line 1 symbol 11):\n\"malformed\"\n"); + let result = endpoint::subscribe::Response::from_string(content); + + match result { + Err(Error(ErrorDetail::Response(e), _)) => { + let response = e.source; + + assert_eq!(response.code(), Code::InternalError); + assert_eq!(response.message(), "Internal error"); + assert_eq!(response.data().unwrap(),"failed to parse query: \nparse error near PegText (line 1 symbol 2 - line 1 symbol 11):\n\"malformed\"\n"); + } + _ => panic!("expected Response error"), + } } "subscribe_newblock" => { - let result = endpoint::subscribe::Response::from_string(content) - .err() - .unwrap(); - assert_eq!(result.code(), Code::ParseError); - assert_eq!(result.message(), "Parse error. Invalid JSON"); - assert_eq!( - result.data().unwrap(), - "missing field `jsonrpc` at line 1 column 2" - ); + let result = endpoint::subscribe::Response::from_string(content); + + match result { + Err(Error(ErrorDetail::Serde(_), _)) => {} + _ => panic!("expected Serde parse error, instead got {:?}", result), + } } "subscribe_newblock_0" => { let result = tendermint_rpc::event::Event::from_string(content).unwrap(); diff --git a/rpc/tests/parse_response.rs b/rpc/tests/parse_response.rs index a96499b01..e3aff8857 100644 --- a/rpc/tests/parse_response.rs +++ b/rpc/tests/parse_response.rs @@ -6,7 +6,11 @@ use tendermint::abci::Code; use std::str::FromStr; use tendermint::vote; use tendermint_rpc::endpoint::consensus_state::RoundVote; -use tendermint_rpc::{self as rpc, endpoint, Response}; +use tendermint_rpc::{ + endpoint, + error::{Error, ErrorDetail}, + Code as RpcCode, Response, +}; const EXAMPLE_APP: &str = "GaiaApp"; const EXAMPLE_CHAIN: &str = "cosmoshub-2"; @@ -294,15 +298,17 @@ fn validators() { fn jsonrpc_error() { let result = endpoint::blockchain::Response::from_string(&read_json_fixture("error")); - if let Err(err) = result { - assert_eq!(err.code(), rpc::error::Code::InternalError); - assert_eq!(err.message(), "Internal error"); - assert_eq!( - err.data().unwrap(), - "min height 321 can't be greater than max height 123" - ); - } else { - panic!("expected error, got {:?}", result) + match result { + Err(Error(ErrorDetail::Response(e), _)) => { + let response = e.source; + assert_eq!(response.code(), RpcCode::InternalError); + assert_eq!(response.message(), "Internal error"); + assert_eq!( + response.data().unwrap(), + "min height 321 can't be greater than max height 123" + ); + } + _ => panic!("expected Response error"), } } diff --git a/std-ext/src/try_clone.rs b/std-ext/src/try_clone.rs index 0e7df29fd..2ea599a76 100644 --- a/std-ext/src/try_clone.rs +++ b/std-ext/src/try_clone.rs @@ -7,7 +7,7 @@ use std::net::TcpStream; pub trait TryClone: Sized { /// The type of error that can be returned when an attempted clone /// operation fails. - type Error; + type Error: std::error::Error; /// Attempt to clone this instance. /// diff --git a/tendermint/Cargo.toml b/tendermint/Cargo.toml index de4646f04..bbb4b5991 100644 --- a/tendermint/Cargo.toml +++ b/tendermint/Cargo.toml @@ -33,10 +33,9 @@ rustdoc-args = ["--cfg", "docsrs"] crate-type = ["cdylib", "rlib"] [dependencies] -anomaly = "0.2" async-trait = "0.1" bytes = "1.0" -chrono = { version = "0.4", features = ["serde"] } +chrono = { version = "0.4.19", features = ["serde"] } ed25519 = "1" ed25519-dalek = { version = "1", features = ["serde"] } futures = "0.3" @@ -52,17 +51,23 @@ sha2 = { version = "0.9", default-features = false } signature = "1.2" subtle = "2" subtle-encoding = { version = "0.5", features = ["bech32-preview"] } -thiserror = "1" tendermint-proto = { version = "0.21.0", path = "../proto" } toml = { version = "0.5" } url = { version = "2.2" } zeroize = { version = "1.1", features = ["zeroize_derive"] } +flex-error = { version = "0.4.1", default-features = false } +time = "0.1.40" k256 = { version = "0.9", optional = true, features = ["ecdsa"] } ripemd160 = { version = "0.9", optional = true } [features] +default = ["std", "eyre_tracer"] +eyre_tracer = ["flex-error/eyre_tracer"] secp256k1 = ["k256", "ripemd160"] +std = [ + "flex-error/std" +] [dev-dependencies] pretty_assertions = "0.7.2" diff --git a/tendermint/src/abci/gas.rs b/tendermint/src/abci/gas.rs index 5ccfce00c..eae908fcc 100644 --- a/tendermint/src/abci/gas.rs +++ b/tendermint/src/abci/gas.rs @@ -5,7 +5,7 @@ //! //! -use crate::{Error, Kind}; +use crate::error::Error; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ fmt::{self, Display}, @@ -45,7 +45,12 @@ impl FromStr for Gas { type Err = Error; fn from_str(s: &str) -> Result { - Ok(Self::from(s.parse::().map_err(|_| Kind::Parse)?)) + let res = s + .parse::() + .map_err(|e| Error::parse_int(s.to_string(), e))? + .into(); + + Ok(res) } } diff --git a/tendermint/src/abci/transaction/hash.rs b/tendermint/src/abci/transaction/hash.rs index a4f946c42..a4fa167cb 100644 --- a/tendermint/src/abci/transaction/hash.rs +++ b/tendermint/src/abci/transaction/hash.rs @@ -1,6 +1,6 @@ //! Transaction hashes -use crate::error::{Error, Kind}; +use crate::error::Error; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::{ fmt::{self, Debug, Display}, @@ -64,10 +64,10 @@ impl FromStr for Hash { // Accept either upper or lower case hex let bytes = hex::decode_upper(s) .or_else(|_| hex::decode(s)) - .map_err(|_| Kind::Parse.context("hash decode"))?; + .map_err(Error::subtle_encoding)?; if bytes.len() != LENGTH { - return Err(Kind::Parse.context("hash length").into()); + return Err(Error::invalid_hash_size()); } let mut result_bytes = [0u8; LENGTH]; diff --git a/tendermint/src/account.rs b/tendermint/src/account.rs index 488ba1769..b79691f7b 100644 --- a/tendermint/src/account.rs +++ b/tendermint/src/account.rs @@ -1,9 +1,6 @@ //! Tendermint accounts -use crate::{ - error::{Error, Kind}, - public_key::Ed25519, -}; +use crate::{error::Error, public_key::Ed25519}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use sha2::{Digest, Sha256}; @@ -36,7 +33,7 @@ impl TryFrom> for Id { fn try_from(value: Vec) -> Result { if value.len() != LENGTH { - return Err(Kind::InvalidAccountIdLength.into()); + return Err(Error::invalid_account_id_length()); } let mut slice: [u8; LENGTH] = [0; LENGTH]; slice.copy_from_slice(&value[..]); @@ -118,7 +115,7 @@ impl FromStr for Id { // Accept either upper or lower case hex let bytes = hex::decode_upper(s) .or_else(|_| hex::decode(s)) - .map_err(|_| Kind::Parse.context("account id decode"))?; + .map_err(Error::subtle_encoding)?; bytes.try_into() } diff --git a/tendermint/src/block.rs b/tendermint/src/block.rs index e3e49485a..fcc70ad7d 100644 --- a/tendermint/src/block.rs +++ b/tendermint/src/block.rs @@ -21,7 +21,7 @@ pub use self::{ round::*, size::Size, }; -use crate::{abci::transaction, evidence, Error, Kind}; +use crate::{abci::transaction, error::Error, evidence}; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use tendermint_proto::types::Block as RawBlock; @@ -55,7 +55,7 @@ impl TryFrom for Block { type Error = Error; fn try_from(value: RawBlock) -> Result { - let header: Header = value.header.ok_or(Kind::MissingHeader)?.try_into()?; + let header: Header = value.header.ok_or_else(Error::missing_header)?.try_into()?; // if last_commit is Commit::Default, it is considered nil by Go. let last_commit = value .last_commit @@ -63,9 +63,9 @@ impl TryFrom for Block { .transpose()? .filter(|c| c != &Commit::default()); if last_commit.is_none() && header.height.value() != 1 { - return Err(Kind::InvalidBlock - .context("last_commit is empty on non-first block") - .into()); + return Err(Error::invalid_block( + "last_commit is empty on non-first block".to_string(), + )); } // Todo: Figure out requirements. //if last_commit.is_some() && header.height.value() == 1 { @@ -74,8 +74,11 @@ impl TryFrom for Block { //} Ok(Block { header, - data: value.data.ok_or(Kind::MissingData)?.try_into()?, - evidence: value.evidence.ok_or(Kind::MissingEvidence)?.try_into()?, + data: value.data.ok_or_else(Error::missing_data)?.into(), + evidence: value + .evidence + .ok_or_else(Error::missing_evidence)? + .try_into()?, last_commit, }) } @@ -101,14 +104,14 @@ impl Block { last_commit: Option, ) -> Result { if last_commit.is_none() && header.height.value() != 1 { - return Err(Kind::InvalidBlock - .context("last_commit is empty on non-first block") - .into()); + return Err(Error::invalid_block( + "last_commit is empty on non-first block".to_string(), + )); } if last_commit.is_some() && header.height.value() == 1 { - return Err(Kind::InvalidBlock - .context("last_commit is filled on first block") - .into()); + return Err(Error::invalid_block( + "last_commit is filled on first block".to_string(), + )); } Ok(Block { header, diff --git a/tendermint/src/block/commit.rs b/tendermint/src/block/commit.rs index 217d1dd18..d4a3187af 100644 --- a/tendermint/src/block/commit.rs +++ b/tendermint/src/block/commit.rs @@ -2,7 +2,7 @@ use crate::block::commit_sig::CommitSig; use crate::block::{Height, Id, Round}; -use crate::{Error, Kind}; +use crate::error::Error; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use tendermint_proto::types::Commit as RawCommit; @@ -40,7 +40,10 @@ impl TryFrom for Commit { Ok(Self { height: value.height.try_into()?, round: value.round.try_into()?, - block_id: value.block_id.ok_or(Kind::InvalidBlock)?.try_into()?, /* gogoproto.nullable = false */ + block_id: value + .block_id + .ok_or_else(|| Error::invalid_block("missing block id".to_string()))? + .try_into()?, /* gogoproto.nullable = false */ signatures: signatures?, }) } diff --git a/tendermint/src/block/commit_sig.rs b/tendermint/src/block/commit_sig.rs index 368e005c7..e5b2a35c2 100644 --- a/tendermint/src/block/commit_sig.rs +++ b/tendermint/src/block/commit_sig.rs @@ -1,7 +1,7 @@ //! CommitSig within Commit +use crate::error::Error; use crate::{account, Signature, Time}; -use crate::{Error, Kind}; use num_traits::ToPrimitive; use std::convert::{TryFrom, TryInto}; use tendermint_proto::types::BlockIdFlag; @@ -74,47 +74,49 @@ impl TryFrom for CommitSig { let timestamp = value.timestamp.unwrap(); // 0001-01-01T00:00:00.000Z translates to EPOCH-62135596800 seconds if timestamp.nanos != 0 || timestamp.seconds != -62135596800 { - return Err(Kind::InvalidTimestamp - .context("absent commitsig has non-zero timestamp") - .into()); + return Err(Error::invalid_timestamp( + "absent commitsig has non-zero timestamp".to_string(), + )); } } if !value.signature.is_empty() { - return Err(Kind::InvalidSignature.into()); + return Err(Error::invalid_signature("empty signature".to_string())); } return Ok(CommitSig::BlockIdFlagAbsent); } if value.block_id_flag == BlockIdFlag::Commit.to_i32().unwrap() { if value.signature.is_empty() { - return Err(Kind::InvalidSignature - .context("regular commitsig has no signature") - .into()); + return Err(Error::invalid_signature( + "regular commitsig has no signature".to_string(), + )); } if value.validator_address.is_empty() { - return Err(Kind::InvalidValidatorAddress.into()); + return Err(Error::invalid_validator_address()); } + let timestamp = value.timestamp.ok_or_else(Error::missing_timestamp)?.into(); + return Ok(CommitSig::BlockIdFlagCommit { validator_address: value.validator_address.try_into()?, - timestamp: value.timestamp.ok_or(Kind::NoTimestamp)?.try_into()?, + timestamp, signature: value.signature.try_into()?, }); } if value.block_id_flag == BlockIdFlag::Nil.to_i32().unwrap() { if value.signature.is_empty() { - return Err(Kind::InvalidSignature - .context("nil commitsig has no signature") - .into()); + return Err(Error::invalid_signature( + "nil commitsig has no signature".to_string(), + )); } if value.validator_address.is_empty() { - return Err(Kind::InvalidValidatorAddress.into()); + return Err(Error::invalid_validator_address()); } return Ok(CommitSig::BlockIdFlagNil { validator_address: value.validator_address.try_into()?, - timestamp: value.timestamp.ok_or(Kind::NoTimestamp)?.try_into()?, + timestamp: value.timestamp.ok_or_else(Error::missing_timestamp)?.into(), signature: value.signature.try_into()?, }); } - Err(Kind::BlockIdFlag.into()) + Err(Error::block_id_flag()) } } diff --git a/tendermint/src/block/header.rs b/tendermint/src/block/header.rs index caeef58ef..6a483850c 100644 --- a/tendermint/src/block/header.rs +++ b/tendermint/src/block/header.rs @@ -1,7 +1,7 @@ //! Block headers use crate::merkle::simple_hash_from_byte_vectors; -use crate::{account, block, chain, AppHash, Error, Hash, Kind, Time}; +use crate::{account, block, chain, AppHash, Error, Hash, Time}; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use tendermint_proto::types::Header as RawHeader; @@ -89,9 +89,7 @@ impl TryFrom for Header { // height").into()); //} if last_block_id.is_some() && height.value() == 1 { - return Err(Kind::InvalidFirstHeader - .context("last_block_id is not null on first height") - .into()); + return Err(Error::invalid_first_header()); } //if last_commit_hash.is_none() && height.value() != 1 { // return Err(Kind::InvalidHeader.context("last_commit_hash is null on non-first @@ -111,10 +109,10 @@ impl TryFrom for Header { // height").into()); //} Ok(Header { - version: value.version.ok_or(Kind::MissingVersion)?.try_into()?, + version: value.version.ok_or_else(Error::missing_version)?.into(), chain_id: value.chain_id.try_into()?, height, - time: value.time.ok_or(Kind::NoTimestamp)?.try_into()?, + time: value.time.ok_or_else(Error::missing_timestamp)?.into(), last_block_id, last_commit_hash, data_hash: if value.data_hash.is_empty() { @@ -208,14 +206,12 @@ pub struct Version { impl Protobuf for Version {} -impl TryFrom for Version { - type Error = anomaly::BoxError; - - fn try_from(value: RawConsensusVersion) -> Result { - Ok(Version { +impl From for Version { + fn from(value: RawConsensusVersion) -> Self { + Version { block: value.block, app: value.app, - }) + } } } diff --git a/tendermint/src/block/height.rs b/tendermint/src/block/height.rs index 5e2ab6eee..2a50b734f 100644 --- a/tendermint/src/block/height.rs +++ b/tendermint/src/block/height.rs @@ -1,4 +1,4 @@ -use crate::error::{Error, Kind}; +use crate::error::Error; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryInto; use std::{ @@ -21,7 +21,7 @@ impl TryFrom for Height { type Error = Error; fn try_from(value: i64) -> Result { - Ok(Height(value.try_into().map_err(|_| Kind::NegativeHeight)?)) + Ok(Height(value.try_into().map_err(Error::negative_height)?)) } } @@ -35,9 +35,9 @@ impl TryFrom for Height { type Error = Error; fn try_from(value: u64) -> Result { - if value > i64::MAX as u64 { - return Err(Kind::IntegerOverflow.into()); - } + // Make sure the u64 value can be converted safely to i64 + let _ival: i64 = value.try_into().map_err(Error::integer_overflow)?; + Ok(Height(value)) } } @@ -102,7 +102,7 @@ impl FromStr for Height { fn from_str(s: &str) -> Result { Height::try_from( s.parse::() - .map_err(|_| Kind::Parse.context("height decode"))?, + .map_err(|e| Error::parse_int(s.to_string(), e))?, ) } } diff --git a/tendermint/src/block/id.rs b/tendermint/src/block/id.rs index a7458ace7..881dc5b8d 100644 --- a/tendermint/src/block/id.rs +++ b/tendermint/src/block/id.rs @@ -1,6 +1,6 @@ use crate::{ block::parts::Header as PartSetHeader, - error::{Error, Kind}, + error::Error, hash::{Algorithm, Hash}, }; use serde::{Deserialize, Serialize}; @@ -64,9 +64,9 @@ impl TryFrom for Id { fn try_from(value: RawBlockId) -> Result { if value.part_set_header.is_none() { - return Err(Kind::InvalidPartSetHeader - .context("part_set_header is None") - .into()); + return Err(Error::invalid_part_set_header( + "part_set_header is None".to_string(), + )); } Ok(Self { hash: value.hash.try_into()?, @@ -103,9 +103,9 @@ impl TryFrom for Id { fn try_from(value: RawCanonicalBlockId) -> Result { if value.part_set_header.is_none() { - return Err(Kind::InvalidPartSetHeader - .context("part_set_header is None") - .into()); + return Err(Error::invalid_part_set_header( + "part_set_header is None".to_string(), + )); } Ok(Self { hash: value.hash.try_into()?, diff --git a/tendermint/src/block/meta.rs b/tendermint/src/block/meta.rs index f12532ec2..2963e5dae 100644 --- a/tendermint/src/block/meta.rs +++ b/tendermint/src/block/meta.rs @@ -1,7 +1,7 @@ //! Block metadata use super::{Header, Id}; -use crate::{Error, Kind}; +use crate::error::Error; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use tendermint_proto::types::BlockMeta as RawMeta; @@ -30,12 +30,12 @@ impl TryFrom for Meta { Ok(Meta { block_id: value .block_id - .ok_or_else(|| Error::from(Kind::InvalidBlock.context("no block_id")))? + .ok_or_else(|| Error::invalid_block("no block_id".to_string()))? .try_into()?, block_size: value.block_size, header: value .header - .ok_or_else(|| Error::from(Kind::InvalidBlock.context("no header")))? + .ok_or_else(|| Error::invalid_block("no header".to_string()))? .try_into()?, num_txs: value.num_txs, }) diff --git a/tendermint/src/block/parts.rs b/tendermint/src/block/parts.rs index 9aa0a7788..b8479e4b8 100644 --- a/tendermint/src/block/parts.rs +++ b/tendermint/src/block/parts.rs @@ -1,9 +1,9 @@ //! Block parts +use crate::error::Error; use crate::hash::Algorithm; use crate::hash::SHA256_HASH_SIZE; use crate::Hash; -use crate::{Error, Kind}; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use tendermint_proto::types::{ @@ -32,7 +32,7 @@ impl TryFrom for Header { fn try_from(value: RawPartSetHeader) -> Result { if !value.hash.is_empty() && value.hash.len() != SHA256_HASH_SIZE { - return Err(Kind::InvalidHashSize.into()); + return Err(Error::invalid_hash_size()); } Ok(Self { total: value.total, @@ -55,7 +55,7 @@ impl TryFrom for Header { fn try_from(value: RawCanonicalPartSetHeader) -> Result { if !value.hash.is_empty() && value.hash.len() != SHA256_HASH_SIZE { - return Err(Kind::InvalidHashSize.into()); + return Err(Error::invalid_hash_size()); } Ok(Self { total: value.total, @@ -77,14 +77,14 @@ impl Header { /// constructor pub fn new(total: u32, hash: Hash) -> Result { if total == 0 && hash != Hash::None { - return Err(Kind::InvalidPartSetHeader - .context("zero total with existing hash") - .into()); + return Err(Error::invalid_part_set_header( + "zero total with existing hash".to_string(), + )); } if total != 0 && hash == Hash::None { - return Err(Kind::InvalidPartSetHeader - .context("non-zero total with empty hash") - .into()); + return Err(Error::invalid_part_set_header( + "non-zero total with empty hash".to_string(), + )); } Ok(Header { total, hash }) } diff --git a/tendermint/src/block/round.rs b/tendermint/src/block/round.rs index 221bb5dd6..6f42468ef 100644 --- a/tendermint/src/block/round.rs +++ b/tendermint/src/block/round.rs @@ -1,4 +1,4 @@ -use crate::error::{Error, Kind}; +use crate::error::Error; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryInto; use std::{ @@ -15,7 +15,7 @@ impl TryFrom for Round { type Error = Error; fn try_from(value: i32) -> Result { - Ok(Round(value.try_into().map_err(|_| Kind::NegativeRound)?)) + Ok(Round(value.try_into().map_err(Error::negative_round)?)) } } @@ -29,9 +29,8 @@ impl TryFrom for Round { type Error = Error; fn try_from(value: u32) -> Result { - if value > i32::MAX as u32 { - return Err(Kind::IntegerOverflow.into()); - } + let _val: i32 = value.try_into().map_err(Error::integer_overflow)?; + Ok(Round(value)) } } @@ -90,7 +89,7 @@ impl FromStr for Round { fn from_str(s: &str) -> Result { Round::try_from( s.parse::() - .map_err(|_| Kind::Parse.context("round decode"))?, + .map_err(|e| Error::parse_int(s.to_string(), e))?, ) } } diff --git a/tendermint/src/block/signed_header.rs b/tendermint/src/block/signed_header.rs index c89f00dec..8aa3a2300 100644 --- a/tendermint/src/block/signed_header.rs +++ b/tendermint/src/block/signed_header.rs @@ -1,7 +1,7 @@ //! SignedHeader contains commit and and block header. //! It is what the rpc endpoint /commit returns and hence can be used by a //! light client. -use crate::{block, Error, Kind}; +use crate::{block, Error}; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use tendermint_proto::types::SignedHeader as RawSignedHeader; @@ -21,8 +21,14 @@ impl TryFrom for SignedHeader { type Error = Error; fn try_from(value: RawSignedHeader) -> Result { - let header = value.header.ok_or(Kind::InvalidSignedHeader)?.try_into()?; - let commit = value.commit.ok_or(Kind::InvalidSignedHeader)?.try_into()?; + let header = value + .header + .ok_or_else(Error::invalid_signed_header)? + .try_into()?; + let commit = value + .commit + .ok_or_else(Error::invalid_signed_header)? + .try_into()?; Self::new(header, commit) // Additional checks } } @@ -40,7 +46,7 @@ impl SignedHeader { /// Constructor. pub fn new(header: block::Header, commit: block::Commit) -> Result { if header.height != commit.height { - return Err(Kind::InvalidSignedHeader.into()); + return Err(Error::invalid_signed_header()); } Ok(Self { header, commit }) } diff --git a/tendermint/src/block/size.rs b/tendermint/src/block/size.rs index 9f033a15c..e29edfb58 100644 --- a/tendermint/src/block/size.rs +++ b/tendermint/src/block/size.rs @@ -1,6 +1,6 @@ //! Block size parameters -use crate::{Error, Kind}; +use crate::error::Error; use std::convert::{TryFrom, TryInto}; use tendermint_proto::Protobuf; use { @@ -42,7 +42,7 @@ impl TryFrom for Size { max_bytes: value .max_bytes .try_into() - .map_err(|_| Self::Error::from(Kind::IntegerOverflow))?, + .map_err(Error::integer_overflow)?, max_gas: value.max_gas, time_iota_ms: Self::default_time_iota_ms(), }) diff --git a/tendermint/src/chain/id.rs b/tendermint/src/chain/id.rs index 0e5c561ba..312a7d89f 100644 --- a/tendermint/src/chain/id.rs +++ b/tendermint/src/chain/id.rs @@ -1,6 +1,6 @@ //! Tendermint blockchain identifiers -use crate::error::{Error, Kind}; +use crate::error::Error; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryFrom; use std::{ @@ -27,13 +27,13 @@ impl TryFrom for Id { fn try_from(value: String) -> Result { if value.is_empty() || value.len() > MAX_LENGTH { - return Err(Kind::Length.into()); + return Err(Error::length()); } for byte in value.as_bytes() { match byte { b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.' => (), - _ => return Err(Kind::Parse.context("chain id charset").into()), + _ => return Err(Error::parse("chain id charset".to_string())), } } @@ -135,6 +135,7 @@ impl<'de> Deserialize<'de> for Id { #[cfg(test)] mod tests { use super::*; + use crate::error::ErrorDetail; const EXAMPLE_CHAIN_ID: &str = "gaia-9000"; @@ -151,18 +152,18 @@ mod tests { #[test] fn rejects_empty_chain_ids() { - assert_eq!( - *"".parse::().unwrap_err().to_string(), - Kind::Length.to_string() - ); + match "".parse::().unwrap_err().detail() { + ErrorDetail::Length(_) => {} + _ => panic!("expected length error"), + } } #[test] fn rejects_overlength_chain_ids() { let overlong_id = String::from_utf8(vec![b'x'; MAX_LENGTH + 1]).unwrap(); - assert_eq!( - *overlong_id.parse::().unwrap_err().to_string(), - Kind::Length.to_string() - ); + match overlong_id.parse::().unwrap_err().detail() { + ErrorDetail::Length(_) => {} + _ => panic!("expected length error"), + } } } diff --git a/tendermint/src/config.rs b/tendermint/src/config.rs index 9b1682661..bf5e59bb2 100644 --- a/tendermint/src/config.rs +++ b/tendermint/src/config.rs @@ -11,12 +11,7 @@ mod priv_validator_key; pub use self::{node_key::NodeKey, priv_validator_key::PrivValidatorKey}; -use crate::{ - error::{Error, Kind}, - genesis::Genesis, - net, node, Moniker, Timeout, -}; -use anomaly::{fail, format_err}; +use crate::{error::Error, genesis::Genesis, net, node, Moniker, Timeout}; use serde::{de, de::Error as _, ser, Deserialize, Serialize}; use std::{ collections::BTreeMap, @@ -109,7 +104,9 @@ pub struct TendermintConfig { impl TendermintConfig { /// Parse Tendermint `config.toml` pub fn parse_toml>(toml_string: T) -> Result { - Ok(toml::from_str(toml_string.as_ref())?) + let res = toml::from_str(toml_string.as_ref()).map_err(Error::toml)?; + + Ok(res) } /// Load `config.toml` from a file @@ -117,14 +114,8 @@ impl TendermintConfig { where P: AsRef, { - let toml_string = fs::read_to_string(path).map_err(|e| { - format_err!( - Kind::Parse, - "couldn't open {}: {}", - path.as_ref().display(), - e - ) - })?; + let toml_string = fs::read_to_string(path) + .map_err(|e| Error::file_io(format!("{}", path.as_ref().display()), e))?; Self::parse_toml(toml_string) } @@ -133,9 +124,11 @@ impl TendermintConfig { pub fn load_genesis_file(&self, home: impl AsRef) -> Result { let path = home.as_ref().join(&self.genesis_file); let genesis_json = fs::read_to_string(&path) - .map_err(|e| format_err!(Kind::Parse, "couldn't open {}: {}", path.display(), e))?; + .map_err(|e| Error::file_io(format!("{}", path.display()), e))?; + + let res = serde_json::from_str(genesis_json.as_ref()).map_err(Error::serde_json)?; - Ok(serde_json::from_str(genesis_json.as_ref())?) + Ok(res) } /// Load `node_key.json` file from the configured location @@ -212,14 +205,17 @@ impl FromStr for LogLevel { global = Some(parts[0].to_owned()); continue; } else if parts.len() != 2 { - fail!(Kind::Parse, "error parsing log level: {}", level); + return Err(Error::parse(format!("error parsing log level: {}", level))); } let key = parts[0].to_owned(); let value = parts[1].to_owned(); if components.insert(key, value).is_some() { - fail!(Kind::Parse, "duplicate log level setting for: {}", level); + return Err(Error::parse(format!( + "duplicate log level setting for: {}", + level + ))); } } @@ -455,7 +451,8 @@ pub struct P2PConfig { /// Maximum number of outbound peers to connect to, excluding persistent peers pub max_num_outbound_peers: u64, - /// List of node IDs, to which a connection will be (re)established ignoring any existing limits + /// List of node IDs, to which a connection will be (re)established ignoring any existing + /// limits #[serde( serialize_with = "serialize_comma_separated_list", deserialize_with = "deserialize_comma_separated_list" @@ -573,10 +570,11 @@ pub struct ConsensusConfig { /// Commit timeout pub timeout_commit: Timeout, - /// How many blocks to look back to check existence of the node's consensus votes before joining consensus - /// When non-zero, the node will panic upon restart + /// How many blocks to look back to check existence of the node's consensus votes before + /// joining consensus When non-zero, the node will panic upon restart /// if the same consensus key was used to sign {double-sign-check-height} last blocks. - /// So, validators should stop the state machine, wait for some blocks, and then restart the state machine to avoid panic. + /// So, validators should stop the state machine, wait for some blocks, and then restart the + /// state machine to avoid panic. pub double_sign_check_height: u64, /// Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) @@ -644,19 +642,20 @@ pub struct InstrumentationConfig { /// statesync configuration options #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct StatesyncConfig { - /// State sync rapidly bootstraps a new node by discovering, fetching, and restoring a state machine - /// snapshot from peers instead of fetching and replaying historical blocks. Requires some peers in - /// the network to take and serve state machine snapshots. State sync is not attempted if the node - /// has any local state (LastBlockHeight > 0). The node will have a truncated block history, - /// starting from the height of the snapshot. + /// State sync rapidly bootstraps a new node by discovering, fetching, and restoring a state + /// machine snapshot from peers instead of fetching and replaying historical blocks. + /// Requires some peers in the network to take and serve state machine snapshots. State + /// sync is not attempted if the node has any local state (LastBlockHeight > 0). The node + /// will have a truncated block history, starting from the height of the snapshot. pub enable: bool, /// RPC servers (comma-separated) for light client verification of the synced state machine and - /// retrieval of state data for node bootstrapping. Also needs a trusted height and corresponding - /// header hash obtained from a trusted source, and a period during which validators can be trusted. + /// retrieval of state data for node bootstrapping. Also needs a trusted height and + /// corresponding header hash obtained from a trusted source, and a period during which + /// validators can be trusted. /// - /// For Cosmos SDK-based chains, trust-period should usually be about 2/3 of the unbonding time (~2 - /// weeks) during which they can be financially punished (slashed) for misbehavior. + /// For Cosmos SDK-based chains, trust-period should usually be about 2/3 of the unbonding time + /// (~2 weeks) during which they can be financially punished (slashed) for misbehavior. #[serde( serialize_with = "serialize_comma_separated_list", deserialize_with = "deserialize_comma_separated_list" @@ -675,8 +674,8 @@ pub struct StatesyncConfig { /// Time to spend discovering snapshots before initiating a restore. pub discovery_time: Timeout, - /// Temporary directory for state sync snapshot chunks, defaults to the OS tempdir (typically /tmp). - /// Will create a new, randomly named directory within, and remove it when done. + /// Temporary directory for state sync snapshot chunks, defaults to the OS tempdir (typically + /// /tmp). Will create a new, randomly named directory within, and remove it when done. pub temp_dir: String, } diff --git a/tendermint/src/config/node_key.rs b/tendermint/src/config/node_key.rs index ffd0e1d42..e1abe7e4a 100644 --- a/tendermint/src/config/node_key.rs +++ b/tendermint/src/config/node_key.rs @@ -1,12 +1,6 @@ //! Node keys -use crate::{ - error::{Error, Kind}, - node, - private_key::PrivateKey, - public_key::PublicKey, -}; -use anomaly::format_err; +use crate::{error::Error, node, private_key::PrivateKey, public_key::PublicKey}; use serde::{Deserialize, Serialize}; use std::{fs, path::Path}; @@ -20,7 +14,8 @@ pub struct NodeKey { impl NodeKey { /// Parse `node_key.json` pub fn parse_json>(json_string: T) -> Result { - Ok(serde_json::from_str(json_string.as_ref())?) + let res = serde_json::from_str(json_string.as_ref()).map_err(Error::serde_json)?; + Ok(res) } /// Load `node_key.json` from a file @@ -28,14 +23,8 @@ impl NodeKey { where P: AsRef, { - let json_string = fs::read_to_string(path).map_err(|e| { - format_err!( - Kind::Parse, - "couldn't open {}: {}", - path.as_ref().display(), - e - ) - })?; + let json_string = fs::read_to_string(path) + .map_err(|e| Error::file_io(format!("{}", path.as_ref().display()), e))?; Self::parse_json(json_string) } diff --git a/tendermint/src/config/priv_validator_key.rs b/tendermint/src/config/priv_validator_key.rs index 784db0039..f6f3d7bc4 100644 --- a/tendermint/src/config/priv_validator_key.rs +++ b/tendermint/src/config/priv_validator_key.rs @@ -1,13 +1,7 @@ //! Validator private keys use crate::public_key::TendermintKey; -use crate::{ - account, - error::{Error, Kind}, - private_key::PrivateKey, - public_key::PublicKey, -}; -use anomaly::format_err; +use crate::{account, error::Error, private_key::PrivateKey, public_key::PublicKey}; use serde::{Deserialize, Serialize}; use std::{fs, path::Path}; @@ -27,7 +21,8 @@ pub struct PrivValidatorKey { impl PrivValidatorKey { /// Parse `priv_validator_key.json` pub fn parse_json>(json_string: T) -> Result { - let result = serde_json::from_str::(json_string.as_ref())?; + let result = + serde_json::from_str::(json_string.as_ref()).map_err(Error::serde_json)?; // Validate that the parsed key type is usable as a consensus key TendermintKey::new_consensus_key(result.priv_key.public_key())?; @@ -40,14 +35,8 @@ impl PrivValidatorKey { where P: AsRef, { - let json_string = fs::read_to_string(path).map_err(|e| { - format_err!( - Kind::Parse, - "couldn't open {}: {}", - path.as_ref().display(), - e - ) - })?; + let json_string = fs::read_to_string(path) + .map_err(|e| Error::file_io(format!("{}", path.as_ref().display()), e))?; Self::parse_json(json_string) } diff --git a/tendermint/src/consensus/params.rs b/tendermint/src/consensus/params.rs index 47f3e5bd7..3408ffe5d 100644 --- a/tendermint/src/consensus/params.rs +++ b/tendermint/src/consensus/params.rs @@ -1,7 +1,7 @@ //! Tendermint consensus parameters +use crate::error::Error; use crate::{block, evidence, public_key}; -use crate::{Error, Kind}; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use tendermint_proto::abci::ConsensusParams as RawParams; @@ -33,17 +33,19 @@ impl TryFrom for Params { fn try_from(value: RawParams) -> Result { Ok(Self { - block: value.block.ok_or(Kind::InvalidBlock)?.try_into()?, - evidence: value.evidence.ok_or(Kind::InvalidEvidence)?.try_into()?, + block: value + .block + .ok_or_else(|| Error::invalid_block("missing block".to_string()))? + .try_into()?, + evidence: value + .evidence + .ok_or_else(Error::invalid_evidence)? + .try_into()?, validator: value .validator - .ok_or(Kind::InvalidValidatorParams)? + .ok_or_else(Error::invalid_validator_params)? .try_into()?, - version: value - .version - .map(TryFrom::try_from) - .transpose() - .map_err(|_| Kind::InvalidVersionParams)?, + version: value.version.map(TryFrom::try_from).transpose()?, }) } } diff --git a/tendermint/src/error.rs b/tendermint/src/error.rs index 60f97fd8a..8f32ce97c 100644 --- a/tendermint/src/error.rs +++ b/tendermint/src/error.rs @@ -1,222 +1,207 @@ //! Error types -use anomaly::{BoxError, Context}; -use thiserror::Error; - use crate::account; use crate::vote; +use alloc::string::String; +use core::num::TryFromIntError; +use flex_error::{define_error, DisplayOnly}; +use std::io::Error as IoError; +use time::OutOfRangeError; -/// Error type -pub type Error = BoxError; +define_error! { + #[derive(Debug, Clone)] + Error { + Crypto + |_| { format_args!("cryptographic error") }, -/// Kinds of errors -#[derive(Clone, Eq, PartialEq, Debug, Error)] -pub enum Kind { - /// Cryptographic operation failed - #[error("cryptographic error")] - Crypto, + InvalidKey + { detail: String } + |e| { format_args!("invalid key: {}", e) }, - /// Malformatted or otherwise invalid cryptographic key - #[error("invalid key")] - InvalidKey, + Io + [ DisplayOnly ] + |_| { format_args!("I/O error") }, - /// Unsupported public key type. - #[error("unsupported key type")] - UnsupportedKeyType, + FileIo + { path: String } + [ DisplayOnly ] + |e| { format_args!("failed to open file: {}", e.path) }, - /// Input/output error - #[error("I/O error")] - Io, + Length + |_| { format_args!("length error") }, - /// Length incorrect or too long - #[error("length error")] - Length, + Parse + { data: String } + | e | { format_args!("error parsing data: {}", e.data) }, - /// Parse error - #[error("parse error")] - Parse, + ParseInt + { data: String } + [ DisplayOnly] + | e | { format_args!("error parsing int data: {}", e.data) }, - /// Network protocol-related errors - #[error("protocol error")] - Protocol, + ParseUrl + [ DisplayOnly ] + |_| { format_args!("error parsing url error") }, - /// Value out-of-range - #[error("value out of range")] - OutOfRange, + Protocol + { detail: String } + |_| { format_args!("protocol error") }, - /// Signature invalid - #[error("bad signature")] - SignatureInvalid, + OutOfRange + [ DisplayOnly ] + |_| { format_args!("value out of range") }, - /// invalid message type - #[error("invalid message type")] - InvalidMessageType, + SignatureInvalid + { detail: String } + |_| { format_args!("bad signature") }, - /// Negative block height - #[error("negative height")] - NegativeHeight, + InvalidMessageType + |_| { format_args!("invalid message type") }, - /// Negative voting round - #[error("negative round")] - NegativeRound, + NegativeHeight + [ DisplayOnly ] + |_| { format_args!("negative height") }, - /// Negative POL round - #[error("negative POL round")] - NegativePolRound, + NegativeRound + [ DisplayOnly ] + |_| { format_args!("negative round") }, - /// Negative validator index in vote - #[error("negative validator index")] - NegativeValidatorIndex, + NegativePolRound + |_| { format_args!("negative POL round") }, - /// Invalid hash size in part_set_header - #[error("invalid hash: expected hash size to be 32 bytes")] - InvalidHashSize, + NegativeValidatorIndex + [ DisplayOnly ] + |_| { format_args!("negative validator index") }, - /// No timestamp in vote or block header - #[error("no timestamp")] - NoTimestamp, - - /// Invalid timestamp - #[error("invalid timestamp")] - InvalidTimestamp, - - /// Invalid account ID length - #[error("invalid account ID length")] - InvalidAccountIdLength, - - /// Invalid signature ID length - #[error("invalid signature ID length")] - InvalidSignatureIdLength, - - /// Overflow during conversion - #[error("integer overflow")] - IntegerOverflow, - - /// No Vote found during conversion - #[error("no vote found")] - NoVoteFound, - - /// No Proposal found during conversion - #[error("no proposal found")] - NoProposalFound, - - /// Invalid AppHash length found during conversion - #[error("invalid app hash Length")] - InvalidAppHashLength, - - /// Invalid PartSetHeader - #[error("invalid part set header")] - InvalidPartSetHeader, - - /// Missing Header in Block - #[error("missing header field")] - MissingHeader, - - /// Missing Data in Block - #[error("missing data field")] - MissingData, - - /// Missing Evidence in Block - #[error("missing evidence field")] - MissingEvidence, - - /// Missing Timestamp in Block - #[error("missing timestamp field")] - MissingTimestamp, - - /// Invalid Block - #[error("invalid block")] - InvalidBlock, - - /// Invalid first Block - #[error("invalid first block")] - InvalidFirstBlock, - - /// Missing Version field - #[error("missing version")] - MissingVersion, - - /// Invalid Header - #[error("invalid header")] - InvalidHeader, - - /// Invalid first Header - #[error("invalid first header")] - InvalidFirstHeader, - - /// Invalid signature in CommitSig - #[error("invalid signature")] - InvalidSignature, - - /// Invalid validator address in CommitSig - #[error("invalid validator address")] - InvalidValidatorAddress, - - /// Invalid Signed Header - #[error("invalid signed header")] - InvalidSignedHeader, - - /// Invalid Evidence - #[error("invalid evidence")] - InvalidEvidence, - - /// Invalid BlockIdFlag - #[error("invalid block id flag")] - BlockIdFlag, - - /// Negative voting power - #[error("negative power")] - NegativePower, - - /// Mismatch between raw voting power and computed one in validator set - #[error("mismatch between raw voting power ({raw}) and computed one ({computed})")] - RawVotingPowerMismatch { - /// raw voting power - raw: vote::Power, - /// computed voting power - computed: vote::Power, - }, - - /// Missing Public Key - #[error("missing public key")] - MissingPublicKey, - - /// Invalid validator parameters - #[error("invalid validator parameters")] - InvalidValidatorParams, - - /// Invalid version parameters - #[error("invalid version parameters")] - InvalidVersionParams, - - /// Negative max_age_num_blocks in Evidence parameters - #[error("negative max_age_num_blocks")] - NegativeMaxAgeNum, - - /// Missing max_age_duration in evidence parameters - #[error("missing max_age_duration")] - MissingMaxAgeDuration, - - /// Proposer not found in validator set - #[error("proposer with address '{}' not found in validator set", _0)] - ProposerNotFound(account::Id), - - /// Trust threshold fraction too large. - #[error("trust threshold is too large (must be <= 1)")] - TrustThresholdTooLarge, - - /// Undefined trust threshold. - #[error("undefined trust threshold (denominator cannot be 0)")] - UndefinedTrustThreshold, - - /// Trust threshold fraction is too small (must be >= 1/3). - #[error("trust threshold too small (must be >= 1/3)")] - TrustThresholdTooSmall, -} + InvalidHashSize + |_| { format_args!("invalid hash: expected hash size to be 32 bytes") }, + + NonZeroTimestamp + | _ | { "absent commitsig has non-zero timestamp" }, + + InvalidAccountIdLength + |_| { format_args!("invalid account ID length") }, + + InvalidSignatureIdLength + |_| { format_args!("invalid signature ID length") }, + + IntegerOverflow + [ DisplayOnly ] + |_| { format_args!("integer overflow") }, + + NoVoteFound + |_| { format_args!("no vote found") }, + + NoProposalFound + |_| { format_args!("no proposal found") }, + + InvalidAppHashLength + |_| { format_args!("invalid app hash length") }, + + InvalidPartSetHeader + { detail : String } + |_| { format_args!("invalid part set header") }, + + MissingHeader + |_| { format_args!("missing header field") }, + + MissingData + |_| { format_args!("missing data field") }, + + MissingEvidence + |_| { format_args!("missing evidence field") }, + + MissingTimestamp + |_| { format_args!("missing timestamp field") }, + + InvalidTimestamp + { reason: String } + | e | { format_args!("invalid timestamp: {}", e.reason) }, + + InvalidBlock + { reason: String } + | e | { format_args!("invalid block: {}", e.reason) }, + + MissingVersion + |_| { format_args!("missing version") }, + + InvalidFirstHeader + |_| { format_args!("last_block_id is not null on first height") }, + + InvalidSignature + { reason: String } + | e | { format_args!("invalid signature: {}", e.reason) }, + + InvalidValidatorAddress + |_| { format_args!("invalid validator address") }, + + InvalidSignedHeader + |_| { format_args!("invalid signed header") }, + + InvalidEvidence + |_| { format_args!("invalid evidence") }, + + BlockIdFlag + |_| { format_args!("invalid block id flag") }, + + NegativePower + [ DisplayOnly ] + |_| { format_args!("negative power") }, + + UnsupportedKeyType + |_| { format_args!("unsupported key type" ) }, + + RawVotingPowerMismatch + { raw: vote::Power, computed: vote::Power } + |e| { format_args!("mismatch between raw voting ({0:?}) and computed one ({1:?})", e.raw, e.computed) }, + + MissingPublicKey + |_| { format_args!("missing public key") }, + + InvalidValidatorParams + |_| { format_args!("invalid validator parameters") }, + + InvalidVersionParams + |_| { format_args!("invalid version parameters") }, + + NegativeMaxAgeNum + [ DisplayOnly ] + |_| { format_args!("negative max_age_num_blocks") }, + + MissingMaxAgeDuration + |_| { format_args!("missing max_age_duration") }, + + ProposerNotFound + { account: account::Id } + |e| { format_args!("proposer with address '{0}' no found in validator set", e.account) }, + + ChronoParse + [ DisplayOnly ] + |_| { format_args!("chrono parse error") }, + + SubtleEncoding + [ DisplayOnly ] + |_| { format_args!("subtle encoding error") }, + + SerdeJson + [ DisplayOnly ] + |_| { format_args!("serde json error") }, + + Toml + [ DisplayOnly ] + |_| { format_args!("toml de error") }, + + Signature + [ DisplayOnly ] + |_| { format_args!("signature error") }, + + TrustThresholdTooLarge + |_| { "trust threshold is too large (must be <= 1)" }, + + UndefinedTrustThreshold + |_| { "undefined trust threshold (denominator cannot be 0)" }, -impl Kind { - /// Add additional context. - pub fn context(self, source: impl Into) -> Context { - Context::new(self, Some(source.into())) + TrustThresholdTooSmall + |_| { "trust threshold too small (must be >= 1/3)" }, } } diff --git a/tendermint/src/evidence.rs b/tendermint/src/evidence.rs index 280199a07..1aca9db12 100644 --- a/tendermint/src/evidence.rs +++ b/tendermint/src/evidence.rs @@ -1,7 +1,7 @@ //! Evidence of malfeasance by validators (i.e. signing conflicting votes). use crate::{ - block::signed_header::SignedHeader, serializers, vote::Power, Error, Kind, Time, Vote, + block::signed_header::SignedHeader, error::Error, serializers, vote::Power, Time, Vote, }; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; @@ -40,7 +40,7 @@ impl TryFrom for Evidence { type Error = Error; fn try_from(value: RawEvidence) -> Result { - match value.sum.ok_or(Kind::InvalidEvidence)? { + match value.sum.ok_or_else(Error::invalid_evidence)? { Sum::DuplicateVoteEvidence(ev) => Ok(Evidence::DuplicateVote(ev.try_into()?)), Sum::LightClientAttackEvidence(_ev) => Ok(Evidence::LightClientAttackEvidence), } @@ -74,11 +74,17 @@ impl TryFrom for DuplicateVoteEvidence { fn try_from(value: RawDuplicateVoteEvidence) -> Result { Ok(Self { - vote_a: value.vote_a.ok_or(Kind::MissingEvidence)?.try_into()?, - vote_b: value.vote_b.ok_or(Kind::MissingEvidence)?.try_into()?, + vote_a: value + .vote_a + .ok_or_else(Error::missing_evidence)? + .try_into()?, + vote_b: value + .vote_b + .ok_or_else(Error::missing_evidence)? + .try_into()?, total_voting_power: value.total_voting_power.try_into()?, validator_power: value.validator_power.try_into()?, - timestamp: value.timestamp.ok_or(Kind::MissingTimestamp)?.try_into()?, + timestamp: value.timestamp.ok_or_else(Error::missing_timestamp)?.into(), }) } } @@ -99,7 +105,7 @@ impl DuplicateVoteEvidence { /// constructor pub fn new(vote_a: Vote, vote_b: Vote) -> Result { if vote_a.height != vote_b.height { - return Err(Kind::InvalidEvidence.into()); + return Err(Error::invalid_evidence()); } // Todo: make more assumptions about what is considered a valid evidence for duplicate vote Ok(Self { @@ -224,10 +230,10 @@ impl TryFrom for Params { max_age_num_blocks: value .max_age_num_blocks .try_into() - .map_err(|_| Self::Error::from(Kind::NegativeMaxAgeNum))?, + .map_err(Error::negative_max_age_num)?, max_age_duration: value .max_age_duration - .ok_or(Kind::MissingMaxAgeDuration)? + .ok_or_else(Error::missing_max_age_duration)? .try_into()?, max_bytes: value.max_bytes, }) @@ -266,14 +272,8 @@ impl TryFrom for Duration { fn try_from(value: RawDuration) -> Result { Ok(Self(std::time::Duration::new( - value - .seconds - .try_into() - .map_err(|_| Self::Error::from(Kind::IntegerOverflow))?, - value - .nanos - .try_into() - .map_err(|_| Self::Error::from(Kind::IntegerOverflow))?, + value.seconds.try_into().map_err(Error::integer_overflow)?, + value.nanos.try_into().map_err(Error::integer_overflow)?, ))) } } diff --git a/tendermint/src/hash.rs b/tendermint/src/hash.rs index d74e3a056..de70237c0 100644 --- a/tendermint/src/hash.rs +++ b/tendermint/src/hash.rs @@ -1,6 +1,6 @@ //! Hash functions and their outputs -use crate::error::{Error, Kind}; +use crate::error::Error; use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryFrom; @@ -66,9 +66,7 @@ impl Hash { h.copy_from_slice(bytes); Ok(Hash::Sha256(h)) } else { - Err(Kind::Parse - .context(format!("hash invalid length: {}", bytes.len())) - .into()) + Err(Error::invalid_hash_size()) } } } @@ -82,7 +80,9 @@ impl Hash { match alg { Algorithm::Sha256 => { let mut h = [0u8; SHA256_HASH_SIZE]; - Hex::upper_case().decode_to_slice(s.as_bytes(), &mut h)?; + Hex::upper_case() + .decode_to_slice(s.as_bytes(), &mut h) + .map_err(Error::subtle_encoding)?; Ok(Hash::Sha256(h)) } } @@ -214,10 +214,12 @@ impl AppHash { /// Decode a `Hash` from upper-case hexadecimal pub fn from_hex_upper(s: &str) -> Result { if s.len() % 2 != 0 { - return Err(Kind::InvalidAppHashLength.into()); + return Err(Error::invalid_app_hash_length()); } let mut h = vec![0; s.len() / 2]; - Hex::upper_case().decode_to_slice(s.as_bytes(), &mut h)?; + Hex::upper_case() + .decode_to_slice(s.as_bytes(), &mut h) + .map_err(Error::subtle_encoding)?; Ok(AppHash(h)) } } diff --git a/tendermint/src/lib.rs b/tendermint/src/lib.rs index ef965e4b3..aed2bd270 100644 --- a/tendermint/src/lib.rs +++ b/tendermint/src/lib.rs @@ -7,7 +7,6 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![deny( warnings, - missing_docs, trivial_casts, trivial_numeric_casts, unused_import_braces, @@ -19,6 +18,8 @@ html_logo_url = "https://raw.githubusercontent.com/informalsystems/tendermint-rs/master/img/logo-tendermint-rs_3961x4001.png" )] +extern crate alloc; + #[macro_use] pub mod error; @@ -53,7 +54,7 @@ mod test; pub use crate::{ block::Block, - error::{Error, Kind}, + error::Error, genesis::Genesis, hash::AppHash, hash::Hash, diff --git a/tendermint/src/net.rs b/tendermint/src/net.rs index 3e48f2322..6e9c4ceff 100644 --- a/tendermint/src/net.rs +++ b/tendermint/src/net.rs @@ -1,9 +1,6 @@ //! Remote addresses (`tcp://` or `unix://`) -use crate::{ - error::{Error, Kind}, - node, -}; +use crate::{error::Error, node}; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ @@ -82,7 +79,7 @@ impl FromStr for Address { // If the address has no scheme, assume it's TCP format!("{}{}", TCP_PREFIX, addr) }; - let url = Url::parse(&prefixed_addr).map_err(|e| Kind::Parse.context(e))?; + let url = Url::parse(&prefixed_addr).map_err(Error::parse_url)?; match url.scheme() { "tcp" => Ok(Self::Tcp { peer_id: if !url.username().is_empty() { @@ -93,19 +90,17 @@ impl FromStr for Address { host: url .host_str() .ok_or_else(|| { - Kind::Parse.context(format!("invalid TCP address (missing host): {}", addr)) + Error::parse(format!("invalid TCP address (missing host): {}", addr)) })? .to_owned(), port: url.port().ok_or_else(|| { - Kind::Parse.context(format!("invalid TCP address (missing port): {}", addr)) + Error::parse(format!("invalid TCP address (missing port): {}", addr)) })?, }), "unix" => Ok(Self::Unix { path: PathBuf::from(url.path()), }), - _ => Err(Kind::Parse - .context(format!("invalid address scheme: {:?}", addr)) - .into()), + _ => Err(Error::parse(format!("invalid address scheme: {:?}", addr))), } } } diff --git a/tendermint/src/node/id.rs b/tendermint/src/node/id.rs index 7cedc02e9..160953c7a 100644 --- a/tendermint/src/node/id.rs +++ b/tendermint/src/node/id.rs @@ -12,7 +12,7 @@ use subtle::{self, ConstantTimeEq}; use subtle_encoding::hex; use crate::{ - error::{Error, Kind}, + error::Error, public_key::{Ed25519, PublicKey}, }; @@ -80,10 +80,10 @@ impl FromStr for Id { // Accept either upper or lower case hex let bytes = hex::decode_upper(s) .or_else(|_| hex::decode(s)) - .map_err(|_| Kind::Parse)?; + .map_err(Error::subtle_encoding)?; if bytes.len() != LENGTH { - return Err(Kind::Parse.into()); + return Err(Error::parse("invalid length".to_string())); } let mut result_bytes = [0u8; LENGTH]; @@ -105,7 +105,7 @@ impl TryFrom for Id { match pk { PublicKey::Ed25519(ed25519) => Ok(Id::from(ed25519)), #[cfg(feature = "secp256k1")] - _ => Err(Kind::UnsupportedKeyType.into()), + _ => Err(Error::unsupported_key_type()), } } } diff --git a/tendermint/src/proposal.rs b/tendermint/src/proposal.rs index 7c3a84685..f2c508f74 100644 --- a/tendermint/src/proposal.rs +++ b/tendermint/src/proposal.rs @@ -11,9 +11,9 @@ pub use sign_proposal::{SignProposalRequest, SignedProposalResponse}; use crate::block::{Height, Id as BlockId, Round}; use crate::chain::Id as ChainId; use crate::consensus::State; +use crate::error::Error; use crate::Signature; use crate::Time; -use crate::{Error, Kind}; use bytes::BufMut; use std::convert::{TryFrom, TryInto}; use tendermint_proto::types::Proposal as RawProposal; @@ -45,7 +45,7 @@ impl TryFrom for Proposal { fn try_from(value: RawProposal) -> Result { if value.pol_round < -1 { - return Err(Kind::NegativePolRound.into()); + return Err(Error::negative_pol_round()); } let pol_round = match value.pol_round { -1 => None, @@ -57,7 +57,7 @@ impl TryFrom for Proposal { round: value.round.try_into()?, pol_round, block_id: value.block_id.map(TryInto::try_into).transpose()?, - timestamp: value.timestamp.map(TryInto::try_into).transpose()?, + timestamp: value.timestamp.map(|t| t.into()), signature: value.signature.try_into()?, }) } diff --git a/tendermint/src/proposal/canonical_proposal.rs b/tendermint/src/proposal/canonical_proposal.rs index 04ad1074d..13de36f93 100644 --- a/tendermint/src/proposal/canonical_proposal.rs +++ b/tendermint/src/proposal/canonical_proposal.rs @@ -3,8 +3,8 @@ use super::Type; use crate::block::{Height, Id as BlockId, Round}; use crate::chain::Id as ChainId; +use crate::error::Error; use crate::Time; -use crate::{Error, Kind}; use std::convert::{TryFrom, TryInto}; use tendermint_proto::types::CanonicalProposal as RawCanonicalProposal; use tendermint_proto::Protobuf; @@ -35,15 +35,13 @@ impl TryFrom for CanonicalProposal { fn try_from(value: RawCanonicalProposal) -> Result { if value.pol_round < -1 { - return Err(Kind::NegativePolRound.into()); + return Err(Error::negative_pol_round()); } - let round = Round::try_from( - i32::try_from(value.round).map_err(|e| Kind::IntegerOverflow.context(e))?, - )?; + let round = Round::try_from(i32::try_from(value.round).map_err(Error::integer_overflow)?)?; let pol_round = match value.pol_round { -1 => None, n => Some(Round::try_from( - i32::try_from(n).map_err(|e| Kind::IntegerOverflow.context(e))?, + i32::try_from(n).map_err(Error::integer_overflow)?, )?), }; // If the Hash is empty in BlockId, the BlockId should be empty. @@ -55,7 +53,7 @@ impl TryFrom for CanonicalProposal { round, pol_round, block_id: block_id.map(TryInto::try_into).transpose()?, - timestamp: value.timestamp.map(TryInto::try_into).transpose()?, + timestamp: value.timestamp.map(|t| t.into()), chain_id: ChainId::try_from(value.chain_id).unwrap(), }) } diff --git a/tendermint/src/proposal/msg_type.rs b/tendermint/src/proposal/msg_type.rs index 32433dda4..e06319944 100644 --- a/tendermint/src/proposal/msg_type.rs +++ b/tendermint/src/proposal/msg_type.rs @@ -1,4 +1,4 @@ -use crate::{Error, Kind}; +use crate::error::Error; use serde::de::Error as _; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryFrom; @@ -20,7 +20,7 @@ impl TryFrom for Type { fn try_from(value: i32) -> Result { match value { 32 => Ok(Type::Proposal), - _ => Err(Kind::InvalidMessageType.into()), + _ => Err(Error::invalid_message_type()), } } } diff --git a/tendermint/src/proposal/sign_proposal.rs b/tendermint/src/proposal/sign_proposal.rs index ccb78398e..e8a23e70a 100644 --- a/tendermint/src/proposal/sign_proposal.rs +++ b/tendermint/src/proposal/sign_proposal.rs @@ -1,6 +1,6 @@ use super::Proposal; use crate::chain::Id as ChainId; -use crate::{Error, Kind}; +use crate::error::Error; use bytes::BufMut; use std::convert::{TryFrom, TryInto}; use tendermint_proto::privval::RemoteSignerError; @@ -26,7 +26,7 @@ impl TryFrom for SignProposalRequest { fn try_from(value: RawSignProposalRequest) -> Result { if value.proposal.is_none() { - return Err(Kind::NoProposalFound.into()); + return Err(Error::no_proposal_found()); } Ok(SignProposalRequest { proposal: Proposal::try_from(value.proposal.unwrap())?, diff --git a/tendermint/src/public_key.rs b/tendermint/src/public_key.rs index c675a8642..ab6b2068c 100644 --- a/tendermint/src/public_key.rs +++ b/tendermint/src/public_key.rs @@ -9,11 +9,7 @@ mod pub_key_response; pub use pub_key_request::PubKeyRequest; pub use pub_key_response::PubKeyResponse; -use crate::{ - error::{self, Error}, - signature::Signature, -}; -use anomaly::{fail, format_err}; +use crate::{error::Error, signature::Signature}; use serde::{de, ser, Deserialize, Serialize}; use signature::Verifier as _; use std::convert::TryFrom; @@ -65,18 +61,17 @@ impl TryFrom for PublicKey { fn try_from(value: RawPublicKey) -> Result { let sum = &value .sum - .ok_or_else(|| format_err!(error::Kind::InvalidKey, "empty sum"))?; + .ok_or_else(|| Error::invalid_key("empty sum".to_string()))?; if let Sum::Ed25519(b) = sum { - return Self::from_raw_ed25519(b).ok_or_else(|| { - format_err!(error::Kind::InvalidKey, "malformed ed25519 key").into() - }); + return Self::from_raw_ed25519(b) + .ok_or_else(|| Error::invalid_key("malformed ed25519 key".to_string())); } #[cfg(feature = "secp256k1")] if let Sum::Secp256k1(b) = sum { return Self::from_raw_secp256k1(b) - .ok_or_else(|| format_err!(error::Kind::InvalidKey, "malformed key").into()); + .ok_or_else(|| Error::invalid_key("malformed key".to_string())); } - Err(format_err!(error::Kind::InvalidKey, "not an ed25519 key").into()) + Err(Error::invalid_key("not an ed25519 key".to_string())) } } @@ -137,21 +132,14 @@ impl PublicKey { match self { PublicKey::Ed25519(pk) => match signature { Signature::Ed25519(sig) => pk.verify(msg, sig).map_err(|_| { - format_err!( - error::Kind::SignatureInvalid, - "Ed25519 signature verification failed" - ) - .into() + Error::signature_invalid("Ed25519 signature verification failed".to_string()) }), - Signature::None => { - Err(format_err!(error::Kind::SignatureInvalid, "missing signature").into()) - } + Signature::None => Err(Error::signature_invalid("missing signature".to_string())), }, #[cfg(feature = "secp256k1")] - PublicKey::Secp256k1(_) => fail!( - error::Kind::InvalidKey, - "unsupported signature algorithm (ECDSA/secp256k1)" - ), + PublicKey::Secp256k1(_) => Err(Error::invalid_key( + "unsupported signature algorithm (ECDSA/secp256k1)".to_string(), + )), } } @@ -250,10 +238,9 @@ impl TendermintKey { #[allow(unreachable_patterns)] match public_key { PublicKey::Ed25519(_) => Ok(TendermintKey::AccountKey(public_key)), - _ => fail!( - error::Kind::InvalidKey, - "only ed25519 consensus keys are supported" - ), + _ => Err(Error::invalid_key( + "only ed25519 consensus keys are supported".to_string(), + )), } } @@ -308,7 +295,7 @@ impl FromStr for Algorithm { match s { "ed25519" => Ok(Algorithm::Ed25519), "secp256k1" => Ok(Algorithm::Secp256k1), - _ => Err(error::Kind::Parse.into()), + _ => Err(Error::parse(format!("invalid algorithm: {}", s))), } } } diff --git a/tendermint/src/signature.rs b/tendermint/src/signature.rs index 77f848d86..90f4a752b 100644 --- a/tendermint/src/signature.rs +++ b/tendermint/src/signature.rs @@ -6,7 +6,7 @@ pub use signature::{Signer, Verifier}; #[cfg(feature = "secp256k1")] pub use k256::ecdsa::Signature as Secp256k1; -use crate::{Error, Kind}; +use crate::error::Error; use std::convert::TryFrom; use tendermint_proto::Protobuf; @@ -31,7 +31,7 @@ impl TryFrom> for Signature { return Ok(Self::default()); } if value.len() != ED25519_SIGNATURE_SIZE { - return Err(Kind::InvalidSignatureIdLength.into()); + return Err(Error::invalid_signature_id_length()); } let mut slice: [u8; ED25519_SIGNATURE_SIZE] = [0; ED25519_SIGNATURE_SIZE]; slice.copy_from_slice(&value[..]); diff --git a/tendermint/src/time.rs b/tendermint/src/time.rs index 12d204b0f..35dc12103 100644 --- a/tendermint/src/time.rs +++ b/tendermint/src/time.rs @@ -1,11 +1,8 @@ //! Timestamps used by Tendermint blockchains -use crate::error::{Error, Kind}; - use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use std::convert::{Infallible, TryFrom}; use std::fmt; use std::ops::{Add, Sub}; use std::str::FromStr; @@ -14,6 +11,8 @@ use tendermint_proto::google::protobuf::Timestamp; use tendermint_proto::serializers::timestamp; use tendermint_proto::Protobuf; +use crate::error::Error; + /// Tendermint timestamps /// #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] @@ -22,10 +21,8 @@ pub struct Time(DateTime); impl Protobuf for Time {} -impl TryFrom for Time { - type Error = Infallible; - - fn try_from(value: Timestamp) -> Result { +impl From for Time { + fn from(value: Timestamp) -> Self { // prost_types::Timestamp has a SystemTime converter but // tendermint_proto::Timestamp can be JSON-encoded let prost_value = prost_types::Timestamp { @@ -33,7 +30,7 @@ impl TryFrom for Time { nanos: value.nanos, }; - Ok(SystemTime::from(prost_value).into()) + SystemTime::from(prost_value).into() } } @@ -66,12 +63,15 @@ impl Time { self.0 .signed_duration_since(other.0) .to_std() - .map_err(|_| Kind::OutOfRange.into()) + .map_err(Error::out_of_range) } /// Parse [`Time`] from an RFC 3339 date pub fn parse_from_rfc3339(s: &str) -> Result { - Ok(Time(DateTime::parse_from_rfc3339(s)?.with_timezone(&Utc))) + let date = DateTime::parse_from_rfc3339(s) + .map_err(Error::chrono_parse)? + .with_timezone(&Utc); + Ok(Time(date)) } /// Return an RFC 3339 and ISO 8601 date and time string with 6 subseconds digits and Z. diff --git a/tendermint/src/timeout.rs b/tendermint/src/timeout.rs index 9a1bc63b3..d4e75e939 100644 --- a/tendermint/src/timeout.rs +++ b/tendermint/src/timeout.rs @@ -1,5 +1,4 @@ -use crate::{Error, Kind}; -use anomaly::{fail, format_err}; +use crate::error::Error; use serde::{de, de::Error as _, ser, Deserialize, Serialize}; use std::{fmt, ops::Deref, str::FromStr, time::Duration}; @@ -34,20 +33,20 @@ impl FromStr for Timeout { fn from_str(s: &str) -> Result { // Timeouts are either 'ms' or 's', and should always end with 's' if s.len() < 2 || !s.ends_with('s') { - fail!(Kind::Parse, "invalid units"); + return Err(Error::parse("invalid units".to_string())); } let units = match s.chars().nth(s.len() - 2) { Some('m') => "ms", Some('0'..='9') => "s", - _ => fail!(Kind::Parse, "invalid units"), + _ => return Err(Error::parse("invalid units".to_string())), }; let numeric_part = s.chars().take(s.len() - units.len()).collect::(); let numeric_value = numeric_part .parse::() - .map_err(|e| format_err!(Kind::Parse, e))?; + .map_err(|e| Error::parse_int(numeric_part, e))?; let duration = match units { "s" => Duration::from_secs(numeric_value), @@ -84,8 +83,7 @@ impl Serialize for Timeout { #[cfg(test)] mod tests { use super::Timeout; - use crate::Kind; - use anomaly::format_err; + use crate::error; #[test] fn parse_seconds() { @@ -101,9 +99,9 @@ mod tests { #[test] fn reject_no_units() { - let expect = format_err!(Kind::Parse, "invalid units").to_string(); - let got = "123".parse::().unwrap_err().to_string(); - - assert_eq!(got, expect); + match "123".parse::().unwrap_err().detail() { + error::ErrorDetail::Parse(_) => {} + _ => panic!("expected parse error to be returned"), + } } } diff --git a/tendermint/src/trust_threshold.rs b/tendermint/src/trust_threshold.rs index 873ad1f39..1f9583540 100644 --- a/tendermint/src/trust_threshold.rs +++ b/tendermint/src/trust_threshold.rs @@ -3,7 +3,7 @@ use std::fmt::{self, Debug, Display}; use crate::error::Error; -use crate::{serializers, Kind}; +use crate::serializers; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::convert::TryFrom; @@ -53,13 +53,13 @@ impl TrustThresholdFraction { /// In any other case we return an error. pub fn new(numerator: u64, denominator: u64) -> Result { if numerator >= denominator { - return Err(Kind::TrustThresholdTooLarge.into()); + return Err(Error::trust_threshold_too_large()); } if denominator == 0 { - return Err(Kind::UndefinedTrustThreshold.into()); + return Err(Error::undefined_trust_threshold()); } if 3 * numerator < denominator { - return Err(Kind::TrustThresholdTooSmall.into()); + return Err(Error::trust_threshold_too_small()); } Ok(Self { numerator, diff --git a/tendermint/src/validator.rs b/tendermint/src/validator.rs index 9d4595a75..36a14ed0d 100644 --- a/tendermint/src/validator.rs +++ b/tendermint/src/validator.rs @@ -3,7 +3,7 @@ use serde::{de::Error as _, Deserialize, Deserializer, Serialize}; use subtle_encoding::base64; -use crate::{account, hash::Hash, merkle, vote, Error, Kind, PublicKey, Signature}; +use crate::{account, hash::Hash, merkle, vote, Error, PublicKey, Signature}; use std::convert::{TryFrom, TryInto}; use tendermint_proto::types::SimpleValidator as RawSimpleValidator; @@ -37,11 +37,10 @@ impl TryFrom for Set { // Ensure that the raw voting power matches the computed one let raw_voting_power = value.total_voting_power.try_into()?; if raw_voting_power != validator_set.total_voting_power() { - return Err(Kind::RawVotingPowerMismatch { - raw: raw_voting_power, - computed: validator_set.total_voting_power(), - } - .into()); + return Err(Error::raw_voting_power_mismatch( + raw_voting_power, + validator_set.total_voting_power(), + )); } Ok(validator_set) @@ -93,7 +92,7 @@ impl Set { .iter() .find(|v| v.address == proposer_address) .cloned() - .ok_or(Kind::ProposerNotFound(proposer_address))?; + .ok_or_else(|| Error::proposer_not_found(proposer_address))?; // Create the validator set with the given proposer. // This is required by IBC on-chain validation. @@ -170,10 +169,13 @@ impl TryFrom for Info { fn try_from(value: RawValidator) -> Result { Ok(Info { address: value.address.try_into()?, - pub_key: value.pub_key.ok_or(Kind::MissingPublicKey)?.try_into()?, + pub_key: value + .pub_key + .ok_or_else(Error::missing_public_key)? + .try_into()?, power: value.voting_power.try_into()?, name: None, - proposer_priority: value.proposer_priority.try_into()?, + proposer_priority: value.proposer_priority.into(), }) } } diff --git a/tendermint/src/vote.rs b/tendermint/src/vote.rs index 8f35e3565..986cbf3af 100644 --- a/tendermint/src/vote.rs +++ b/tendermint/src/vote.rs @@ -11,9 +11,9 @@ pub use self::sign_vote::*; pub use self::validator_index::ValidatorIndex; use crate::chain::Id as ChainId; use crate::consensus::State; +use crate::error::Error; use crate::hash; use crate::{account, block, Signature, Time}; -use crate::{Error, Kind::*}; use bytes::BufMut; use ed25519::Signature as ed25519Signature; use ed25519::SIGNATURE_LENGTH as ed25519SignatureLength; @@ -65,7 +65,7 @@ impl TryFrom for Vote { fn try_from(value: RawVote) -> Result { if value.timestamp.is_none() { - return Err(NoTimestamp.into()); + return Err(Error::missing_timestamp()); } Ok(Vote { vote_type: value.r#type.try_into()?, @@ -77,7 +77,7 @@ impl TryFrom for Vote { .map(TryInto::try_into) .transpose()? .filter(|i| i != &block::Id::default()), - timestamp: value.timestamp.map(TryInto::try_into).transpose()?, + timestamp: value.timestamp.map(|t| t.into()), validator_address: value.validator_address.try_into()?, validator_index: value.validator_index.try_into()?, signature: value.signature.try_into()?, @@ -231,7 +231,7 @@ impl TryFrom for Type { match value { 1 => Ok(Type::Prevote), 2 => Ok(Type::Precommit), - _ => Err(InvalidMessageType.into()), + _ => Err(Error::invalid_message_type()), } } } @@ -259,7 +259,7 @@ impl FromStr for Type { match s { "Prevote" => Ok(Self::Prevote), "Precommit" => Ok(Self::Precommit), - _ => Err(InvalidMessageType.into()), + _ => Err(Error::invalid_message_type()), } } } diff --git a/tendermint/src/vote/canonical_vote.rs b/tendermint/src/vote/canonical_vote.rs index 170af775b..92cfa6e62 100644 --- a/tendermint/src/vote/canonical_vote.rs +++ b/tendermint/src/vote/canonical_vote.rs @@ -1,6 +1,6 @@ use crate::chain::Id as ChainId; +use crate::error::Error; use crate::{block, Time}; -use crate::{Error, Kind::*}; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use tendermint_proto::types::CanonicalVote as RawCanonicalVote; @@ -37,12 +37,10 @@ impl TryFrom for CanonicalVote { fn try_from(value: RawCanonicalVote) -> Result { if value.timestamp.is_none() { - return Err(NoTimestamp.into()); - } - if value.round > i32::MAX as i64 { - // CanonicalVote uses sfixed64, Vote uses int32. They translate to u64 vs i32 in Rust. - return Err(IntegerOverflow.into()); + return Err(Error::missing_timestamp()); } + let _val: i32 = value.round.try_into().map_err(Error::integer_overflow)?; + // If the Hash is empty in BlockId, the BlockId should be empty. // See: https://github.com/informalsystems/tendermint-rs/issues/663 let block_id = value.block_id.filter(|i| !i.hash.is_empty()); @@ -50,8 +48,8 @@ impl TryFrom for CanonicalVote { vote_type: value.r#type.try_into()?, height: value.height.try_into()?, round: (value.round as i32).try_into()?, - block_id: block_id.map(TryInto::try_into).transpose()?, - timestamp: value.timestamp.map(TryInto::try_into).transpose()?, + block_id: block_id.map(|b| b.try_into()).transpose()?, + timestamp: value.timestamp.map(|t| t.into()), chain_id: ChainId::try_from(value.chain_id)?, }) } diff --git a/tendermint/src/vote/power.rs b/tendermint/src/vote/power.rs index dba8ac304..86b4bc09e 100644 --- a/tendermint/src/vote/power.rs +++ b/tendermint/src/vote/power.rs @@ -5,7 +5,7 @@ use std::fmt; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; -use crate::{Error, Kind}; +use crate::error::Error; /// Voting power #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Default)] @@ -21,7 +21,7 @@ impl TryFrom for Power { type Error = Error; fn try_from(value: i64) -> Result { - Ok(Power(value.try_into().map_err(|_| Kind::NegativePower)?)) + Ok(Power(value.try_into().map_err(Error::negative_power)?)) } } @@ -35,9 +35,8 @@ impl TryFrom for Power { type Error = Error; fn try_from(value: u64) -> Result { - if value > i64::MAX as u64 { - return Err(Kind::IntegerOverflow.into()); - } + let _val: i64 = value.try_into().map_err(Error::integer_overflow)?; + Ok(Power(value)) } } diff --git a/tendermint/src/vote/sign_vote.rs b/tendermint/src/vote/sign_vote.rs index 9db358e70..4d07ce394 100644 --- a/tendermint/src/vote/sign_vote.rs +++ b/tendermint/src/vote/sign_vote.rs @@ -1,8 +1,8 @@ use crate::chain; +use crate::error::Error; use crate::Vote; -use crate::{Error, Kind}; use bytes::BufMut; -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use tendermint_proto::privval::SignedVoteResponse as RawSignedVoteResponse; use tendermint_proto::privval::{RemoteSignerError, SignVoteRequest as RawSignVoteRequest}; use tendermint_proto::Error as ProtobufError; @@ -23,13 +23,11 @@ impl TryFrom for SignVoteRequest { type Error = Error; fn try_from(value: RawSignVoteRequest) -> Result { - if value.vote.is_none() { - return Err(Kind::NoVoteFound.into()); - } - Ok(SignVoteRequest { - vote: Vote::try_from(value.vote.unwrap())?, - chain_id: chain::Id::try_from(value.chain_id)?, - }) + let vote = value.vote.ok_or_else(Error::no_vote_found)?.try_into()?; + + let chain_id = value.chain_id.try_into()?; + + Ok(SignVoteRequest { vote, chain_id }) } } diff --git a/tendermint/src/vote/validator_index.rs b/tendermint/src/vote/validator_index.rs index d928e28db..e11ad1834 100644 --- a/tendermint/src/vote/validator_index.rs +++ b/tendermint/src/vote/validator_index.rs @@ -1,4 +1,4 @@ -use crate::error::{Error, Kind}; +use crate::error::Error; use std::convert::TryInto; use std::{ convert::TryFrom, @@ -15,7 +15,7 @@ impl TryFrom for ValidatorIndex { fn try_from(value: i32) -> Result { Ok(ValidatorIndex( - value.try_into().map_err(|_| Kind::NegativeValidatorIndex)?, + value.try_into().map_err(Error::negative_validator_index)?, )) } } @@ -30,9 +30,7 @@ impl TryFrom for ValidatorIndex { type Error = Error; fn try_from(value: u32) -> Result { - if value > i32::MAX as u32 { - return Err(Kind::IntegerOverflow.into()); - } + let _val: i32 = value.try_into().map_err(Error::integer_overflow)?; Ok(ValidatorIndex(value)) } } @@ -48,7 +46,7 @@ impl TryFrom for ValidatorIndex { fn try_from(value: usize) -> Result { Ok(ValidatorIndex( - value.try_into().map_err(|_| Kind::IntegerOverflow)?, + value.try_into().map_err(Error::integer_overflow)?, )) } } @@ -87,7 +85,7 @@ impl FromStr for ValidatorIndex { fn from_str(s: &str) -> Result { ValidatorIndex::try_from( s.parse::() - .map_err(|_| Kind::Parse.context("validator index decode"))?, + .map_err(|e| Error::parse_int("validator index decode".to_string(), e))?, ) } } diff --git a/test/Cargo.toml b/test/Cargo.toml index 4cea36167..f9bbaa5a8 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -15,12 +15,11 @@ test = true [dev-dependencies] ed25519-dalek = "1" -eyre = "0.6" +flex-error = "0.4.2" flume = "0.10" rand_core = { version = "0.5", features = ["std"] } readwrite = "^0.1.1" subtle-encoding = { version = "0.5" } -thiserror = "1" x25519-dalek = "1.1" tendermint = { path = "../tendermint" } diff --git a/test/src/pipe.rs b/test/src/pipe.rs index 1318295e4..d245db67b 100644 --- a/test/src/pipe.rs +++ b/test/src/pipe.rs @@ -61,7 +61,8 @@ pub fn async_pipe_buffered() -> (Reader, BufWriter) { ) } -/// Creates a pair of pipes for bidirectional communication using buffered writer, a bit like UNIX's `socketpair(2)`. +/// Creates a pair of pipes for bidirectional communication using buffered writer, a bit like UNIX's +/// `socketpair(2)`. pub fn async_bipipe_buffered() -> ( readwrite::ReadWrite, readwrite::ReadWrite, diff --git a/test/src/test/unit/p2p/secret_connection.rs b/test/src/test/unit/p2p/secret_connection.rs index 9582d0a5d..797a29d60 100644 --- a/test/src/test/unit/p2p/secret_connection.rs +++ b/test/src/test/unit/p2p/secret_connection.rs @@ -4,7 +4,6 @@ use std::net::{TcpListener, TcpStream}; use std::thread; use ed25519_dalek::{self as ed25519}; -use eyre::Result; use rand_core::OsRng; use x25519_dalek::PublicKey as EphemeralPublic; @@ -180,7 +179,9 @@ fn test_split_secret_connection() { peer1.join().expect("peer 1's thread to run to completion") } -fn new_peer_conn(io_handler: IoHandler) -> Result> +fn new_peer_conn( + io_handler: IoHandler, +) -> Result, tendermint_p2p::error::Error> where IoHandler: std::io::Read + std::io::Write + Send + Sync, { diff --git a/tools/kvstore-test/src/lib.rs b/tools/kvstore-test/src/lib.rs index f1a7e29e0..eaed3171b 100644 --- a/tools/kvstore-test/src/lib.rs +++ b/tools/kvstore-test/src/lib.rs @@ -5,11 +5,10 @@ //! cargo test //! This will execute all integration tests against the RPC endpoint. //! -//! Option 2: the docker daemon is installed and accessible on the machine where the test will happen -//! for example: on a developer machine +//! Option 2: the docker daemon is installed and accessible on the machine where the test will +//! happen for example: on a developer machine //! Run: //! cargo make //! This will start a docker container with Tendermint and attach port 26657 to the host machine. //! Then it will run all tests against the freshly created endpoint. //! Make sure you installed cargo-make by running `cargo install cargo-make` first. -//! diff --git a/tools/kvstore-test/tests/light-client.rs b/tools/kvstore-test/tests/light-client.rs index a9ac55220..c81185794 100644 --- a/tools/kvstore-test/tests/light-client.rs +++ b/tools/kvstore-test/tests/light-client.rs @@ -11,7 +11,6 @@ //! cargo make //! //! (Make sure you install cargo-make using `cargo install cargo-make` first.) -//! use tendermint_light_client::{ builder::{LightClientBuilder, SupervisorBuilder}, diff --git a/tools/kvstore-test/tests/tendermint.rs b/tools/kvstore-test/tests/tendermint.rs index 9871c1fae..fb33143fb 100644 --- a/tools/kvstore-test/tests/tendermint.rs +++ b/tools/kvstore-test/tests/tendermint.rs @@ -13,7 +13,6 @@ /// cargo make /// /// (Make sure you install cargo-make using `cargo install cargo-make` first.) -/// mod rpc { use std::cmp::min; diff --git a/tools/proto-compiler/src/functions.rs b/tools/proto-compiler/src/functions.rs index c5a05bffa..a3e992049 100644 --- a/tools/proto-compiler/src/functions.rs +++ b/tools/proto-compiler/src/functions.rs @@ -122,8 +122,8 @@ fn find_reference_or_commit<'a>( tried_origin = true; if try_reference.is_err() { // Remote branch not found, last chance: try as a commit ID - // Note: Oid::from_str() currently does an incorrect conversion and cuts the second half of the ID. - // We are falling back on Oid::from_bytes() for now. + // Note: Oid::from_str() currently does an incorrect conversion and cuts the second half + // of the ID. We are falling back on Oid::from_bytes() for now. let commitish_vec = hex::decode(commitish).unwrap_or_else(|_| hex::decode_upper(commitish).unwrap()); return ( diff --git a/tools/rpc-probe/src/client.rs b/tools/rpc-probe/src/client.rs index 96cb2282e..905cbe5e2 100644 --- a/tools/rpc-probe/src/client.rs +++ b/tools/rpc-probe/src/client.rs @@ -132,7 +132,6 @@ enum DriverCommand { Terminate, } -#[derive(Debug)] pub struct ClientDriver { stream: WebSocketStream, cmd_rx: DriverCommandRx,