diff --git a/Cargo.lock b/Cargo.lock index e16c7cd9f8..11790d648e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5424,6 +5424,7 @@ dependencies = [ "async-trait", "bech32", "bytes", + "chrono", "cnidarium", "decaf377-fmd", "decaf377-rdsa", @@ -5441,6 +5442,9 @@ dependencies = [ "serde_json", "subtle-encoding", "tendermint", + "tendermint-proto", + "tendermint-rpc", + "thiserror", "tonic", "tower", "tracing", @@ -5674,6 +5678,7 @@ dependencies = [ "pin-project", "pin-project-lite", "sha2 0.10.8", + "tap", "tendermint", "tendermint-config", "tendermint-light-client-verifier", diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index 1193d8172e..4096be7491 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -10,12 +10,19 @@ anyhow = "1" rpc = ["dep:tonic", "ibc-proto/client"] box-grpc = ["dep:http-body", "dep:tonic", "dep:tower"] cnidarium = ["dep:cnidarium"] +tendermint = [ + "dep:chrono", + "dep:tendermint-proto", + "dep:tendermint-rpc", + "dep:thiserror" +] [dependencies] anyhow = {workspace = true} async-trait = {workspace = true} bech32 = {workspace = true} bytes = {workspace = true, features = ["serde"]} +chrono = {workspace = true, optional = true, default-features = false, features = ["serde"]} cnidarium = {workspace = true, optional = true, default-features = true} decaf377-fmd = {workspace = true} decaf377-rdsa = {workspace = true} @@ -32,6 +39,9 @@ serde = {workspace = true, features = ["derive"]} serde_json = {workspace = true} subtle-encoding = "0.5" tendermint = {workspace = true} +tendermint-proto = {workspace = true, optional = true} +tendermint-rpc = {workspace = true, optional = true, features = ["http-client"]} +thiserror = {workspace = true, optional = true} tonic = {workspace = true, optional = true} tower = {workspace = true, features = ["full"], optional = true} tracing = {workspace = true} diff --git a/crates/proto/src/protobuf.rs b/crates/proto/src/protobuf.rs index 4b9d127f33..06817232aa 100644 --- a/crates/proto/src/protobuf.rs +++ b/crates/proto/src/protobuf.rs @@ -1,6 +1,9 @@ use crate::Name; use std::convert::{From, TryFrom}; +#[cfg(feature = "tendermint")] +mod tendermint_compat; + /// A marker type that captures the relationships between a domain type (`Self`) and a protobuf type (`Self::Proto`). pub trait DomainType where diff --git a/crates/proto/src/protobuf/tendermint_compat.rs b/crates/proto/src/protobuf/tendermint_compat.rs new file mode 100644 index 0000000000..5a555d6980 --- /dev/null +++ b/crates/proto/src/protobuf/tendermint_compat.rs @@ -0,0 +1,710 @@ +//! Facilities to help interoperate with [`tendermint`] and [`tendermint_rpc`] types. +// +// NOTE: this submodule is tightly focused on helping `penumbra-tendermint-proxy` function. +// this is not an exhaustive pass at providing compatibility between all of the types in either +// library. accordingly, it is grouped by conversions needed for each RPC endpoint. + +use crate::util::tendermint_proxy::v1 as penumbra_pb; + +// === get_tx === + +impl From for penumbra_pb::GetTxResponse { + fn from( + tendermint_rpc::endpoint::tx::Response { + hash, + height, + index, + tx_result, + tx, + proof: _, + }: tendermint_rpc::endpoint::tx::Response, + ) -> Self { + Self { + height: height.value(), + index: index as u64, + hash: hash.as_bytes().to_vec(), + tx_result: Some(tx_result.into()), + tx, + } + } +} + +impl From for penumbra_pb::TxResult { + fn from( + tendermint::abci::types::ExecTxResult { + log, + gas_wanted, + gas_used, + events, + code: _, + data: _, + info: _, + codespace: _, + }: tendermint::abci::types::ExecTxResult, + ) -> Self { + use tendermint::abci::Event; + Self { + log: log.to_string(), + // TODO: validation here, fix mismatch between i64 <> u64 + gas_wanted: gas_wanted as u64, + gas_used: gas_used as u64, + tags: events + .into_iter() + .flat_map(|Event { attributes, .. }: Event| { + attributes.into_iter().map(penumbra_pb::Tag::from) + }) + .collect(), + } + } +} + +impl From for penumbra_pb::Tag { + fn from( + tendermint::abci::EventAttribute { + key, + value, + index: _, + }: tendermint::abci::EventAttribute, + ) -> Self { + Self { + key: key.into_bytes(), + value: value.into_bytes(), + // TODO(kate): this was set to false previously, but it should probably use the + // index field from the tendermint object. for now, carry out a refactor and avoid + // changing behavior while doing so. + index: false, + } + } +} + +// === broadcast_tx_async === + +impl From + for penumbra_pb::BroadcastTxAsyncResponse +{ + fn from( + tendermint_rpc::endpoint::broadcast::tx_async::Response { + code, + data, + log, + hash, + }: tendermint_rpc::endpoint::broadcast::tx_async::Response, + ) -> Self { + Self { + code: u32::from(code) as u64, + data: data.to_vec(), + log, + hash: hash.as_bytes().to_vec(), + } + } +} + +// === broadcast_tx_sync === + +impl From + for penumbra_pb::BroadcastTxSyncResponse +{ + fn from( + tendermint_rpc::endpoint::broadcast::tx_sync::Response { + code, + data, + log, + hash, + }: tendermint_rpc::endpoint::broadcast::tx_sync::Response, + ) -> Self { + Self { + code: u32::from(code) as u64, + data: data.to_vec(), + log, + hash: hash.as_bytes().to_vec(), + } + } +} + +// === get_status === + +impl From for penumbra_pb::GetStatusResponse { + fn from( + tendermint_rpc::endpoint::status::Response { + node_info, + sync_info, + validator_info, + }: tendermint_rpc::endpoint::status::Response, + ) -> Self { + Self { + node_info: Some(node_info.into()), + sync_info: Some(sync_info.into()), + validator_info: Some(validator_info.into()), + } + } +} + +impl From for crate::tendermint::p2p::DefaultNodeInfo { + fn from( + tendermint::node::Info { + protocol_version, + id, + listen_addr, + network, + version, + channels, + moniker, + other, + }: tendermint::node::Info, + ) -> Self { + Self { + protocol_version: Some(protocol_version.into()), + default_node_id: id.to_string(), + listen_addr: listen_addr.to_string(), + network: network.to_string(), + version: version.to_string(), + channels: channels.to_string().as_bytes().to_vec(), + moniker: moniker.to_string(), + other: Some(crate::tendermint::p2p::DefaultNodeInfoOther { + tx_index: match other.tx_index { + tendermint::node::info::TxIndexStatus::On => "on".to_string(), + tendermint::node::info::TxIndexStatus::Off => "off".to_string(), + }, + rpc_address: other.rpc_address.to_string(), + }), + } + } +} + +impl From for penumbra_pb::SyncInfo { + fn from( + tendermint_rpc::endpoint::status::SyncInfo { + latest_block_hash, + latest_app_hash, + latest_block_height, + latest_block_time, + catching_up, + earliest_block_hash: _, + earliest_app_hash: _, + earliest_block_height: _, + earliest_block_time: _, + }: tendermint_rpc::endpoint::status::SyncInfo, + ) -> Self { + // The tendermint-rs `Timestamp` type is a newtype wrapper + // around a `time::PrimitiveDateTime` however it's private so we + // have to use string parsing to get to the prost type we want :( + let latest_block_time = + chrono::DateTime::parse_from_rfc3339(latest_block_time.to_rfc3339().as_str()) + .expect("timestamp should roundtrip to string"); + + Self { + latest_block_hash: latest_block_hash.to_string().as_bytes().to_vec(), + latest_app_hash: latest_app_hash.to_string().as_bytes().to_vec(), + latest_block_height: latest_block_height.value(), + latest_block_time: Some(pbjson_types::Timestamp { + seconds: latest_block_time.timestamp(), + nanos: latest_block_time.timestamp_subsec_nanos() as i32, + }), + catching_up, + // These don't exist in tendermint-rpc right now. + // earliest_app_hash: res.sync_info.earliest_app_hash.to_string().as_bytes().to_vec(), + // earliest_block_hash: res.sync_info.earliest_block_hash.to_string().as_bytes().to_vec(), + // earliest_block_height: res.sync_info.earliest_block_height.value(), + // earliest_block_time: Some(pbjson_types::Timestamp{ + // seconds: earliest_block_time.timestamp(), + // nanos: earliest_block_time.timestamp_nanos() as i32, + // }), + } + } +} + +impl From for crate::tendermint::types::Validator { + fn from( + tendermint::validator::Info { + address, + pub_key, + power, + proposer_priority, + name: _, + }: tendermint::validator::Info, + ) -> Self { + use crate::tendermint::crypto::{public_key::Sum::Ed25519, PublicKey}; + Self { + address: address.to_string().as_bytes().to_vec(), + pub_key: Some(PublicKey { + sum: Some(Ed25519(pub_key.to_bytes().to_vec())), + }), + voting_power: power.into(), + proposer_priority: proposer_priority.into(), + } + } +} + +impl From for crate::tendermint::p2p::ProtocolVersion { + fn from( + tendermint::node::info::ProtocolVersionInfo { + p2p, + block, + app + }: tendermint::node::info::ProtocolVersionInfo, + ) -> Self { + Self { p2p, block, app } + } +} + +// === abci_query === + +#[derive(Debug, thiserror::Error)] +#[error("height '{height}' from tendermint overflowed i64, this should never happen")] +pub struct HeightOverflowError { + height: u64, + #[source] + source: >::Error, +} + +impl TryFrom for penumbra_pb::AbciQueryResponse { + type Error = HeightOverflowError; + fn try_from( + tendermint_rpc::endpoint::abci_query::AbciQuery { + code, + log, + info, + index, + key, + value, + proof, + height, + codespace, + }: tendermint_rpc::endpoint::abci_query::AbciQuery, + ) -> Result { + let proof_ops = proof.map(crate::tendermint::crypto::ProofOps::from); + let height = i64::try_from(height.value()).map_err(|source| HeightOverflowError { + height: height.value(), + source, + })?; + Ok(Self { + code: u32::from(code), + log, + info, + index, + key, + value, + proof_ops, + height, + codespace, + }) + } +} + +impl From for crate::tendermint::crypto::ProofOps { + fn from( + tendermint::merkle::proof::ProofOps { ops }: tendermint::merkle::proof::ProofOps, + ) -> Self { + Self { + ops: ops + .into_iter() + .map(crate::tendermint::crypto::ProofOp::from) + .collect(), + } + } +} + +impl From for crate::tendermint::crypto::ProofOp { + fn from( + tendermint::merkle::proof::ProofOp { + field_type, + key, + data, + }: tendermint::merkle::proof::ProofOp, + ) -> Self { + Self { + r#type: field_type, + key, + data, + } + } +} + +// === get_block_by_height === + +impl TryFrom for penumbra_pb::GetBlockByHeightResponse { + // TODO(kate): ideally this would not return a tonic status object, but we'll use this for + // now to avoid invasively refactoring this code. + type Error = tonic::Status; + fn try_from( + tendermint_rpc::endpoint::block::Response { + block, + block_id, + }: tendermint_rpc::endpoint::block::Response, + ) -> Result { + Ok(Self { + block: block.try_into().map(Some)?, + block_id: Some(block_id.into()), + }) + } +} + +impl TryFrom for crate::tendermint::types::Block { + // TODO(kate): ideally this would not return a tonic status object, but we'll use this for + // now to avoid invasively refactoring this code. + type Error = tonic::Status; + fn try_from( + tendermint::Block { + header, + data, + evidence, + last_commit, + .. + }: tendermint::Block, + ) -> Result { + Ok(crate::tendermint::types::Block { + header: header.try_into().map(Some)?, + data: Some(crate::tendermint::types::Data { txs: data }), + evidence: evidence.try_into().map(Some)?, + last_commit: Some( + last_commit + .map(crate::tendermint::types::Commit::try_from) + .transpose()? + // TODO(kate): this probably should not panic, but this is here to preserve + // existing behavior. panic if no last commit is set. + .expect("last_commit"), + ), + }) + } +} + +impl TryFrom for crate::tendermint::types::Header { + // TODO(kate): ideally this would not return a tonic status object, but we'll use this for + // now to avoid invasively refactoring this code. + type Error = tonic::Status; + fn try_from( + tendermint::block::Header { + version, + chain_id, + height, + time, + last_block_id, + last_commit_hash, + data_hash, + validators_hash, + next_validators_hash, + consensus_hash, + app_hash, + last_results_hash, + evidence_hash, + proposer_address, + }: tendermint::block::Header, + ) -> Result { + // The tendermint-rs `Timestamp` type is a newtype wrapper + // around a `time::PrimitiveDateTime` however it's private so we + // have to use string parsing to get to the prost type we want :( + let header_time = chrono::DateTime::parse_from_rfc3339(time.to_rfc3339().as_str()) + .expect("timestamp should roundtrip to string"); + Ok(Self { + version: Some(crate::tendermint::version::Consensus { + block: version.block, + app: version.app, + }), + chain_id: chain_id.into(), + height: height.into(), + time: Some(pbjson_types::Timestamp { + seconds: header_time.timestamp(), + nanos: header_time + .timestamp_nanos_opt() + .ok_or_else(|| tonic::Status::invalid_argument("missing header_time nanos"))? + as i32, + }), + last_block_id: last_block_id.map(|id| crate::tendermint::types::BlockId { + hash: id.hash.into(), + part_set_header: Some(crate::tendermint::types::PartSetHeader { + total: id.part_set_header.total, + hash: id.part_set_header.hash.into(), + }), + }), + last_commit_hash: last_commit_hash.map(Into::into).unwrap_or_default(), + data_hash: data_hash.map(Into::into).unwrap_or_default(), + validators_hash: validators_hash.into(), + next_validators_hash: next_validators_hash.into(), + consensus_hash: consensus_hash.into(), + app_hash: app_hash.into(), + last_results_hash: last_results_hash.map(Into::into).unwrap_or_default(), + evidence_hash: evidence_hash.map(Into::into).unwrap_or_default(), + proposer_address: proposer_address.into(), + }) + } +} + +impl TryFrom for crate::tendermint::types::EvidenceList { + // TODO(kate): ideally this would not return a tonic status object, but we'll use this for + // now to avoid invasively refactoring this code. + type Error = tonic::Status; + fn try_from(list: tendermint::evidence::List) -> Result { + list.into_vec() + .into_iter() + .map(crate::tendermint::types::Evidence::try_from) + .collect::, _>>() + .map(|evidence| Self { evidence }) + } +} + +// TODO(kate): this should be decomposed further at a later point, i am refraining from doing +// so right now. there are `Option::expect()` calls below that should be considered. +impl TryFrom for crate::tendermint::types::Evidence { + // TODO(kate): ideally this would not return a tonic status object, but we'll use this for + // now to avoid invasively refactoring this code. + type Error = tonic::Status; + fn try_from(evidence: tendermint::evidence::Evidence) -> Result { + use {chrono::DateTime, std::ops::Deref}; + Ok(Self { + sum: Some(match evidence { + tendermint::evidence::Evidence::DuplicateVote(e) => { + let e2 = + tendermint_proto::types::DuplicateVoteEvidence::from(e.deref().clone()); + crate::tendermint::types::evidence::Sum::DuplicateVoteEvidence( + crate::tendermint::types::DuplicateVoteEvidence { + vote_a: Some(crate::tendermint::types::Vote { + r#type: match e.votes().0.vote_type { + tendermint::vote::Type::Prevote => { + crate::tendermint::types::SignedMsgType::Prevote as i32 + } + tendermint::vote::Type::Precommit => { + crate::tendermint::types::SignedMsgType::Precommit as i32 + } + }, + height: e.votes().0.height.into(), + round: e.votes().0.round.into(), + block_id: Some(crate::tendermint::types::BlockId { + hash: e.votes().0.block_id.expect("block id").hash.into(), + part_set_header: Some( + crate::tendermint::types::PartSetHeader { + total: e + .votes() + .0 + .block_id + .expect("block id") + .part_set_header + .total, + hash: e + .votes() + .0 + .block_id + .expect("block id") + .part_set_header + .hash + .into(), + }, + ), + }), + timestamp: Some(pbjson_types::Timestamp { + seconds: DateTime::parse_from_rfc3339( + &e.votes().0.timestamp.expect("timestamp").to_rfc3339(), + ) + .expect("timestamp should roundtrip to string") + .timestamp(), + nanos: DateTime::parse_from_rfc3339( + &e.votes().0.timestamp.expect("timestamp").to_rfc3339(), + ) + .expect("timestamp should roundtrip to string") + .timestamp_nanos_opt() + .ok_or_else(|| { + tonic::Status::invalid_argument("missing timestamp nanos") + })? as i32, + }), + validator_address: e.votes().0.validator_address.into(), + validator_index: e.votes().0.validator_index.into(), + signature: e + .votes() + .0 + .signature + .clone() + .expect("signed vote") + .into(), + }), + vote_b: Some(crate::tendermint::types::Vote { + r#type: match e.votes().1.vote_type { + tendermint::vote::Type::Prevote => { + crate::tendermint::types::SignedMsgType::Prevote as i32 + } + tendermint::vote::Type::Precommit => { + crate::tendermint::types::SignedMsgType::Precommit as i32 + } + }, + height: e.votes().1.height.into(), + round: e.votes().1.round.into(), + block_id: Some(crate::tendermint::types::BlockId { + hash: e.votes().1.block_id.expect("block id").hash.into(), + part_set_header: Some( + crate::tendermint::types::PartSetHeader { + total: e + .votes() + .1 + .block_id + .expect("block id") + .part_set_header + .total, + hash: e + .votes() + .1 + .block_id + .expect("block id") + .part_set_header + .hash + .into(), + }, + ), + }), + timestamp: Some(pbjson_types::Timestamp { + seconds: DateTime::parse_from_rfc3339( + &e.votes().1.timestamp.expect("timestamp").to_rfc3339(), + ) + .expect("timestamp should roundtrip to string") + .timestamp(), + nanos: DateTime::parse_from_rfc3339( + &e.votes().1.timestamp.expect("timestamp").to_rfc3339(), + ) + .expect("timestamp should roundtrip to string") + .timestamp_nanos_opt() + .ok_or_else(|| { + tonic::Status::invalid_argument("missing timestamp nanos") + })? as i32, + }), + validator_address: e.votes().1.validator_address.into(), + validator_index: e.votes().1.validator_index.into(), + signature: e + .votes() + .1 + .signature + .clone() + .expect("signed vote") + .into(), + }), + total_voting_power: e2.total_voting_power, + validator_power: e2.validator_power, + timestamp: e2.timestamp.map(|t| pbjson_types::Timestamp { + seconds: t.seconds, + nanos: t.nanos, + }), + }, + ) + } + tendermint::evidence::Evidence::LightClientAttack(e) => { + use crate::Message; + let e2 = + tendermint_proto::types::LightClientAttackEvidence::from(e.deref().clone()); + let e2_bytes = e2.encode_to_vec(); + let e3 = crate::tendermint::types::LightClientAttackEvidence::decode( + e2_bytes.as_slice(), + ) + .expect("can decode encoded data"); + crate::tendermint::types::evidence::Sum::LightClientAttackEvidence(e3) + } + }), + }) + } +} + +impl TryFrom for crate::tendermint::types::Commit { + // TODO(kate): ideally this would not return a tonic status object, but we'll use this for + // now to avoid invasively refactoring this code. + type Error = tonic::Status; + fn try_from( + tendermint::block::Commit { + height, + round, + block_id, + signatures, + }: tendermint::block::Commit, + ) -> Result { + Ok(Self { + height: height.into(), + round: round.into(), + block_id: Some(block_id.into()), + signatures: signatures + .into_iter() + .map(crate::tendermint::types::CommitSig::try_from) + .collect::>()?, + }) + } +} + +impl TryFrom for crate::tendermint::types::CommitSig { + // TODO(kate): ideally this would not return a tonic status object, but we'll use this for + // now to avoid invasively refactoring this code. + type Error = tonic::Status; + fn try_from(signature: tendermint::block::CommitSig) -> Result { + use chrono::DateTime; + Ok({ + match signature { + tendermint::block::CommitSig::BlockIdFlagAbsent => { + crate::tendermint::types::CommitSig { + block_id_flag: crate::tendermint::types::BlockIdFlag::Absent as i32, + // No validator address, or timestamp is recorded for this variant. Not sure if this is a bug in tendermint-rs or not. + validator_address: vec![], + timestamp: None, + signature: vec![], + } + } + tendermint::block::CommitSig::BlockIdFlagCommit { + validator_address, + timestamp, + signature, + } => crate::tendermint::types::CommitSig { + block_id_flag: crate::tendermint::types::BlockIdFlag::Commit as i32, + validator_address: validator_address.into(), + timestamp: Some(pbjson_types::Timestamp { + seconds: DateTime::parse_from_rfc3339(×tamp.to_rfc3339()) + .expect("timestamp should roundtrip to string") + .timestamp(), + nanos: DateTime::parse_from_rfc3339(×tamp.to_rfc3339()) + .expect("timestamp should roundtrip to string") + .timestamp_nanos_opt() + .ok_or_else(|| { + tonic::Status::invalid_argument("missing timestamp nanos") + })? as i32, + }), + signature: signature.expect("signature").into(), + }, + tendermint::block::CommitSig::BlockIdFlagNil { + validator_address, + timestamp, + signature, + } => crate::tendermint::types::CommitSig { + block_id_flag: crate::tendermint::types::BlockIdFlag::Nil as i32, + validator_address: validator_address.into(), + timestamp: Some(pbjson_types::Timestamp { + seconds: DateTime::parse_from_rfc3339(×tamp.to_rfc3339()) + .expect("timestamp should roundtrip to string") + .timestamp(), + nanos: DateTime::parse_from_rfc3339(×tamp.to_rfc3339()) + .expect("timestamp should roundtrip to string") + .timestamp_nanos_opt() + .ok_or_else(|| { + tonic::Status::invalid_argument("missing timestamp nanos") + })? as i32, + }), + signature: signature.expect("signature").into(), + }, + } + }) + } +} + +impl From for crate::tendermint::types::BlockId { + fn from( + tendermint::block::Id { + hash, + part_set_header, + }: tendermint::block::Id, + ) -> Self { + Self { + hash: hash.into(), + part_set_header: Some(part_set_header.into()), + } + } +} + +impl From for crate::tendermint::types::PartSetHeader { + fn from( + tendermint::block::parts::Header { total, hash, .. }: tendermint::block::parts::Header, + ) -> Self { + Self { + total, + hash: hash.into(), + } + } +} diff --git a/crates/util/tendermint-proxy/Cargo.toml b/crates/util/tendermint-proxy/Cargo.toml index 8bc62e8081..8157b35cd1 100644 --- a/crates/util/tendermint-proxy/Cargo.toml +++ b/crates/util/tendermint-proxy/Cargo.toml @@ -1,31 +1,36 @@ [package] name = "penumbra-tendermint-proxy" -version = {workspace = true} -edition = {workspace = true} +authors.workspace = true +edition.workspace = true +version.workspace = true +repository.workspace = true +homepage.workspace = true +license.workspace = true [dependencies] -anyhow = {workspace = true} -chrono = {workspace = true, default-features = false, features = ["serde"]} -futures = {workspace = true} -hex = {workspace = true} -http = {workspace = true} -metrics = {workspace = true} -pbjson-types = {workspace = true} -penumbra-proto = {workspace = true, features = ["rpc"], default-features = true} -penumbra-transaction = {workspace = true, default-features = true} -pin-project = {workspace = true} -pin-project-lite = {workspace = true} -sha2 = {workspace = true} -tendermint = {workspace = true} -tendermint-config = {workspace = true} -tendermint-light-client-verifier = {workspace = true} -tendermint-proto = {workspace = true} -tendermint-rpc = {workspace = true, features = ["http-client"]} -tokio = {workspace = true, features = ["full"]} -tokio-stream = {workspace = true} -tokio-util = {workspace = true} -tonic = {workspace = true} -tower = {workspace = true, features = ["full"]} -tower-service = {workspace = true} -tracing = {workspace = true} -url = {workspace = true} +anyhow = { workspace = true } +chrono = { workspace = true, default-features = false, features = ["serde"] } +futures = { workspace = true } +hex = { workspace = true } +http = { workspace = true } +metrics = { workspace = true } +pbjson-types = { workspace = true } +penumbra-proto = { workspace = true, features = ["rpc", "tendermint"] } +penumbra-transaction = { workspace = true } +pin-project = { workspace = true } +pin-project-lite = { workspace = true } +sha2 = { workspace = true } +tap = { workspace = true } +tendermint = { workspace = true } +tendermint-config = { workspace = true } +tendermint-light-client-verifier = { workspace = true } +tendermint-proto = { workspace = true } +tendermint-rpc = { workspace = true, features = ["http-client"] } +tokio = { workspace = true, features = ["full"] } +tokio-stream = { workspace = true } +tokio-util = { workspace = true } +tonic = { workspace = true } +tower = { workspace = true, features = ["full"] } +tower-service = { workspace = true } +tracing = { workspace = true } +url = { workspace = true } diff --git a/crates/util/tendermint-proxy/src/lib.rs b/crates/util/tendermint-proxy/src/lib.rs index 027f1ee95c..9f31c155aa 100644 --- a/crates/util/tendermint-proxy/src/lib.rs +++ b/crates/util/tendermint-proxy/src/lib.rs @@ -1,4 +1,28 @@ #![deny(clippy::unwrap_used)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] + +//! Facilities for proxying gRPC requests to an upstream Tendermint/CometBFT RPC. +//! +//! Most importantly, this crate provides [`TendermintProxy`], which implements Penumbra's +//! [`tendermint_proxy`][proxy-proto] RPC. +//! +//! [proxy-proto]: https://buf.build/penumbra-zone/penumbra/docs/main:penumbra.util.tendermint_proxy.v1 + mod tendermint_proxy; -pub use tendermint_proxy::TendermintProxy; + +/// Implements service traits for Tonic gRPC services. +/// +/// The fields of this struct are the configuration and data +/// necessary to the gRPC services. +#[derive(Clone, Debug)] +pub struct TendermintProxy { + /// Address of upstream Tendermint server to proxy requests to. + tendermint_url: url::Url, +} + +impl TendermintProxy { + /// Returns a new [`TendermintProxy`]. + pub fn new(tendermint_url: url::Url) -> Self { + Self { tendermint_url } + } +} diff --git a/crates/util/tendermint-proxy/src/tendermint_proxy.rs b/crates/util/tendermint-proxy/src/tendermint_proxy.rs index 3aa2dc1d2c..fff613c409 100644 --- a/crates/util/tendermint-proxy/src/tendermint_proxy.rs +++ b/crates/util/tendermint-proxy/src/tendermint_proxy.rs @@ -1,33 +1,19 @@ -use chrono::DateTime; -use penumbra_proto::{self as proto, DomainType, Message}; -use penumbra_transaction::Transaction; -use proto::util::tendermint_proxy::v1::{ - tendermint_proxy_service_server::TendermintProxyService, AbciQueryRequest, AbciQueryResponse, - BroadcastTxAsyncRequest, BroadcastTxAsyncResponse, BroadcastTxSyncRequest, - BroadcastTxSyncResponse, GetBlockByHeightRequest, GetBlockByHeightResponse, GetStatusRequest, - GetStatusResponse, GetTxRequest, GetTxResponse, SyncInfo, Tag, TxResult, +use crate::TendermintProxy; +use penumbra_proto::{ + util::tendermint_proxy::v1::{ + tendermint_proxy_service_server::TendermintProxyService, AbciQueryRequest, + AbciQueryResponse, BroadcastTxAsyncRequest, BroadcastTxAsyncResponse, + BroadcastTxSyncRequest, BroadcastTxSyncResponse, GetBlockByHeightRequest, + GetBlockByHeightResponse, GetStatusRequest, GetStatusResponse, GetTxRequest, GetTxResponse, + }, + DomainType, }; -use std::ops::Deref; +use penumbra_transaction::Transaction; +use tap::TapFallible; use tendermint::{abci::Code, block::Height}; use tendermint_rpc::{Client, HttpClient}; use tonic::Status; - -/// Implements service traits for Tonic gRPC services. -/// -/// The fields of this struct are the configuration and data -/// necessary to the gRPC services. -#[derive(Clone, Debug)] -pub struct TendermintProxy { - /// Address of upstream Tendermint server to proxy requests to. - tendermint_url: url::Url, -} - -impl TendermintProxy { - /// Returns a new [`TendermintProxy`]. - pub fn new(tendermint_url: url::Url) -> Self { - Self { tendermint_url } - } -} +use tracing::instrument; #[tonic::async_trait] impl TendermintProxyService for TendermintProxy { @@ -37,190 +23,119 @@ impl TendermintProxyService for TendermintProxy { // since none of the structs are defined in our crates :( // TODO: move those to proto/src/protobuf.rs + /// Fetches a transaction by hash. + /// + /// Returns a [`GetTxResponse`] information about the requested transaction. + #[instrument(level = "info", skip_all)] async fn get_tx( &self, req: tonic::Request, ) -> Result, Status> { - let client = HttpClient::new(self.tendermint_url.to_string().as_ref()).map_err(|e| { - tonic::Status::unavailable(format!("error creating tendermint http client: {e:#?}")) + // Create an HTTP client, connecting to tendermint. + let client = HttpClient::new(self.tendermint_url.as_ref()).map_err(|e| { + Status::unavailable(format!("error creating tendermint http client: {e:#?}")) })?; - let req = req.into_inner(); - let hash = req.hash; - let prove = req.prove; + // Parse the inbound transaction hash from the client request. + let GetTxRequest { hash, prove } = req.into_inner(); + let hash = hash + .try_into() + .map_err(|e| Status::invalid_argument(format!("invalid transaction hash: {e:#?}")))?; + + // Send the request to Tendermint. let rsp = client - .tx( - hash.try_into().map_err(|e| { - tonic::Status::invalid_argument(format!("invalid transaction hash: {e:#?}")) - })?, - prove, - ) + .tx(hash, prove) .await - .map_err(|e| tonic::Status::unavailable(format!("error getting tx: {e}")))?; + .map(GetTxResponse::from) + .map_err(|e| Status::unavailable(format!("error getting tx: {e}")))?; - let tx = Transaction::decode(rsp.tx.as_ref()) - .map_err(|e| tonic::Status::unavailable(format!("error decoding tx: {e}")))?; + // Before forwarding along the response, verify that the transaction can be + // successfully decoded into our domain type. + Transaction::decode(rsp.tx.as_ref()) + .map_err(|e| Status::unavailable(format!("error decoding tx: {e}")))?; - Ok(tonic::Response::new(GetTxResponse { - tx: tx.into(), - tx_result: Some(TxResult { - log: rsp.tx_result.log.to_string(), - // TODO: validation here, fix mismatch between i64 <> u64 - gas_wanted: rsp.tx_result.gas_wanted as u64, - gas_used: rsp.tx_result.gas_used as u64, - tags: rsp - .tx_result - .events - .iter() - .flat_map(|e| { - let a = &e.attributes; - a.iter().map(move |a| { - Tag { - key: a.key.to_string().as_bytes().to_vec(), - value: a.value.to_string().as_bytes().to_vec(), - // TODO: not sure where this index value comes from - index: false, - } - }) - }) - .collect(), - }), - height: rsp.height.value(), - index: rsp.index as u64, - hash: rsp.hash.as_bytes().to_vec(), - })) + Ok(tonic::Response::new(rsp)) } + /// Broadcasts a transaction asynchronously. + #[instrument( + level = "info", + skip_all, + fields(req_id = tracing::field::Empty), + )] async fn broadcast_tx_async( &self, req: tonic::Request, ) -> Result, Status> { - let client = HttpClient::new(self.tendermint_url.to_string().as_ref()).map_err(|e| { - tonic::Status::unavailable(format!("error creating tendermint http client: {e:#?}")) + // Create an HTTP client, connecting to tendermint. + let client = HttpClient::new(self.tendermint_url.as_ref()).map_err(|e| { + Status::unavailable(format!("error creating tendermint http client: {e:#?}")) })?; - let params = req.into_inner().params; + // Process the inbound request, recording the request ID in the tracing span. + let BroadcastTxAsyncRequest { req_id, params } = req.into_inner(); + tracing::Span::current().record("req_id", req_id); - let res = client + // Broadcast the transaction parameters. + client .broadcast_tx_async(params) .await - .map_err(|e| tonic::Status::unavailable(format!("error broadcasting tx async: {e}")))?; - - Ok(tonic::Response::new(BroadcastTxAsyncResponse { - code: u32::from(res.code) as u64, - data: res.data.to_vec(), - log: res.log.to_string(), - hash: res.hash.as_bytes().to_vec(), - })) + .map(BroadcastTxAsyncResponse::from) + .map(tonic::Response::new) + .map_err(|e| Status::unavailable(format!("error broadcasting tx async: {e}"))) } + // Broadcasts a transaction synchronously. + #[instrument( + level = "info", + skip_all, + fields(req_id = tracing::field::Empty), + )] async fn broadcast_tx_sync( &self, req: tonic::Request, ) -> Result, Status> { - let client = HttpClient::new(self.tendermint_url.to_string().as_ref()).map_err(|e| { - tonic::Status::unavailable(format!("error creating tendermint http client: {e:#?}")) + // Create an HTTP client, connecting to tendermint. + let client = HttpClient::new(self.tendermint_url.as_ref()).map_err(|e| { + Status::unavailable(format!("error creating tendermint http client: {e:#?}")) })?; - let res = client - .broadcast_tx_sync(req.into_inner().params) - .await - .map_err(|e| tonic::Status::unavailable(format!("error broadcasting tx sync: {e}")))?; + // Process the inbound request, recording the request ID in the tracing span. + let BroadcastTxSyncRequest { req_id, params } = req.into_inner(); + tracing::Span::current().record("req_id", req_id); - tracing::debug!("{:?}", res); - Ok(tonic::Response::new(BroadcastTxSyncResponse { - code: u32::from(res.code) as u64, - data: res.data.to_vec(), - log: res.log.to_string(), - hash: res.hash.as_bytes().to_vec(), - })) + // Broadcast the transaction parameters. + client + .broadcast_tx_sync(params) + .await + .map(BroadcastTxSyncResponse::from) + .map(tonic::Response::new) + .map_err(|e| tonic::Status::unavailable(format!("error broadcasting tx sync: {e}"))) + .tap_ok(|res| tracing::debug!("{:?}", res)) } + // Queries the current status. + #[instrument(level = "info", skip_all)] async fn get_status( &self, _req: tonic::Request, ) -> Result, Status> { // generic bounds on HttpClient::new are not well-constructed, so we have to // render the URL as a String, then borrow it, then re-parse the borrowed &str - let client = HttpClient::new(self.tendermint_url.to_string().as_ref()).map_err(|e| { + let client = HttpClient::new(self.tendermint_url.as_ref()).map_err(|e| { tonic::Status::unavailable(format!("error creating tendermint http client: {e:#?}")) })?; - let res = client + // Send the status request. + client .status() .await - .map_err(|e| tonic::Status::unavailable(format!("error querying status: {e}")))?; - - // The tendermint-rs `Timestamp` type is a newtype wrapper - // around a `time::PrimitiveDateTime` however it's private so we - // have to use string parsing to get to the prost type we want :( - let latest_block_time = - DateTime::parse_from_rfc3339(&res.sync_info.latest_block_time.to_rfc3339()) - .expect("timestamp should roundtrip to string"); - Ok(tonic::Response::new(GetStatusResponse { - node_info: Some(penumbra_proto::tendermint::p2p::DefaultNodeInfo { - protocol_version: Some(penumbra_proto::tendermint::p2p::ProtocolVersion { - p2p: res.node_info.protocol_version.p2p, - block: res.node_info.protocol_version.block, - app: res.node_info.protocol_version.app, - }), - default_node_id: res.node_info.id.to_string(), - listen_addr: res.node_info.listen_addr.to_string(), - network: res.node_info.network.to_string(), - version: res.node_info.version.to_string(), - channels: res.node_info.channels.to_string().as_bytes().to_vec(), - moniker: res.node_info.moniker.to_string(), - other: Some(penumbra_proto::tendermint::p2p::DefaultNodeInfoOther { - tx_index: match res.node_info.other.tx_index { - tendermint::node::info::TxIndexStatus::On => "on".to_string(), - tendermint::node::info::TxIndexStatus::Off => "off".to_string(), - }, - rpc_address: res.node_info.other.rpc_address.to_string(), - }), - }), - sync_info: Some(SyncInfo { - latest_block_hash: res - .sync_info - .latest_block_hash - .to_string() - .as_bytes() - .to_vec(), - latest_app_hash: res - .sync_info - .latest_app_hash - .to_string() - .as_bytes() - .to_vec(), - latest_block_height: res.sync_info.latest_block_height.value(), - latest_block_time: Some(pbjson_types::Timestamp { - seconds: latest_block_time.timestamp(), - nanos: latest_block_time.timestamp_subsec_nanos() as i32, - }), - // These don't exist in tendermint-rpc right now. - // earliest_app_hash: res.sync_info.earliest_app_hash.to_string().as_bytes().to_vec(), - // earliest_block_hash: res.sync_info.earliest_block_hash.to_string().as_bytes().to_vec(), - // earliest_block_height: res.sync_info.earliest_block_height.value(), - // earliest_block_time: Some(pbjson_types::Timestamp{ - // seconds: earliest_block_time.timestamp(), - // nanos: earliest_block_time.timestamp_nanos() as i32, - // }), - catching_up: res.sync_info.catching_up, - }), - validator_info: Some(penumbra_proto::tendermint::types::Validator { - address: res.validator_info.address.to_string().as_bytes().to_vec(), - pub_key: Some(penumbra_proto::tendermint::crypto::PublicKey { - sum: Some( - penumbra_proto::tendermint::crypto::public_key::Sum::Ed25519( - res.validator_info.pub_key.to_bytes().to_vec(), - ), - ), - }), - voting_power: res.validator_info.power.into(), - proposer_priority: res.validator_info.proposer_priority.into(), - }), - })) + .map(GetStatusResponse::from) + .map(tonic::Response::new) + .map_err(|e| tonic::Status::unavailable(format!("error querying status: {e}"))) } + #[instrument(level = "info", skip_all)] async fn abci_query( &self, req: tonic::Request, @@ -231,52 +146,37 @@ impl TendermintProxyService for TendermintProxy { tonic::Status::unavailable(format!("error creating tendermint http client: {e:#?}")) })?; + // Parse the inbound request, confirm that the height provided is valid. // TODO: how does path validation work on tendermint-rs@29 - let path = req.get_ref().path.clone(); - let data = &req.get_ref().data; - let height: Height = req - .get_ref() - .height + let AbciQueryRequest { + data, + path, + height, + prove, + } = req.into_inner(); + let height: Height = height .try_into() - .map_err(|_| tonic::Status::invalid_argument("invalid height"))?; - let prove = req.get_ref().prove; - let res = client - .abci_query(Some(path), data.clone(), Some(height), prove) - .await - .map_err(|e| tonic::Status::unavailable(format!("error querying abci: {e}")))?; + .map_err(|_| Status::invalid_argument("invalid height"))?; - match res.code { - Code::Ok => Ok(tonic::Response::new(AbciQueryResponse { - code: u32::from(res.code), - log: res.log.to_string(), - info: res.info, - index: res.index, - key: res.key, - value: res.value, - proof_ops: res.proof.map(|p| proto::tendermint::crypto::ProofOps { - ops: p - .ops - .into_iter() - .map(|op| proto::tendermint::crypto::ProofOp { - r#type: op.field_type, - key: op.key, - data: op.data, - }) - .collect(), - }), - height: i64::try_from(res.height.value()).map_err(|_| { - tonic::Status::internal( - "height from tendermint overflowed i64, this should never happen", - ) - })?, - codespace: res.codespace, - })), - tendermint::abci::Code::Err(e) => Err(tonic::Status::unavailable(format!( - "error querying abci: {e}" - ))), - } + // Send the ABCI query to Tendermint. + let rsp = client + .abci_query(Some(path), data, Some(height), prove) + .await + .map_err(|e| Status::unavailable(format!("error querying abci: {e}"))) + // Confirm that the response code is 0, or return an error response. + .and_then(|rsp| match rsp.code { + Code::Ok => Ok(rsp), + tendermint::abci::Code::Err(e) => { + Err(Status::unavailable(format!("error querying abci: {e}"))) + } + })?; + + AbciQueryResponse::try_from(rsp) + .map(tonic::Response::new) + .map_err(|error| Status::internal(format!("{error}"))) } + #[instrument(level = "info", skip_all)] async fn get_block_by_height( &self, req: tonic::Request, @@ -287,208 +187,17 @@ impl TendermintProxyService for TendermintProxy { tonic::Status::unavailable(format!("error creating tendermint http client: {e:#?}")) })?; - let res = client - .block( - tendermint::block::Height::try_from(req.get_ref().height) - .expect("height should be less than 2^63"), - ) - .await - .map_err(|e| tonic::Status::unavailable(format!("error querying abci: {e}")))?; - - // TODO: these conversions exist because the penumbra proto files define - // their own proxy methods, since tendermint odesn't include them, and this results - // in duplicated proto types relative to the tendermint-proto ones. - - // The tendermint-rs `Timestamp` type is a newtype wrapper - // around a `time::PrimitiveDateTime` however it's private so we - // have to use string parsing to get to the prost type we want :( - let header_time = DateTime::parse_from_rfc3339(&res.block.header.time.to_rfc3339()) - .expect("timestamp should roundtrip to string"); - Ok(tonic::Response::new(GetBlockByHeightResponse { - block_id: Some(penumbra_proto::tendermint::types::BlockId { - hash: res.block_id.hash.into(), - part_set_header: Some(penumbra_proto::tendermint::types::PartSetHeader { - total: res.block_id.part_set_header.total, - hash: res.block_id.part_set_header.hash.into(), - }), - }), - block: Some(proto::tendermint::types::Block { - header: Some(proto::tendermint::types::Header { - version: Some(penumbra_proto::tendermint::version::Consensus { - block: res.block.header.version.block, - app: res.block.header.version.app, - }), - chain_id: res.block.header.chain_id.into(), - height: res.block.header.height.into(), - time: Some(pbjson_types::Timestamp { - seconds: header_time.timestamp(), - nanos: header_time.timestamp_nanos_opt().ok_or_else(|| tonic::Status::invalid_argument("missing header_time nanos"))? as i32, - }), - last_block_id: res.block.header.last_block_id.map(|id| { - penumbra_proto::tendermint::types::BlockId { - hash: id.hash.into(), - part_set_header: Some( - penumbra_proto::tendermint::types::PartSetHeader { - total: id.part_set_header.total, - hash: id.part_set_header.hash.into(), - }, - ), - } - }), - last_commit_hash: res - .block - .header - .last_commit_hash - .map(Into::into) - .unwrap_or_default(), - data_hash: res - .block - .header - .data_hash - .map(Into::into) - .unwrap_or_default(), - validators_hash: res.block.header.validators_hash.into(), - next_validators_hash: res.block.header.next_validators_hash.into(), - consensus_hash: res.block.header.consensus_hash.into(), - app_hash: res.block.header.app_hash.into(), - last_results_hash: res - .block - .header - .last_results_hash - .map(Into::into) - .unwrap_or_default(), - evidence_hash: res - .block - .header - .evidence_hash - .map(Into::into) - .unwrap_or_default(), - proposer_address: res.block.header.proposer_address.into(), - }), - data: Some(proto::tendermint::types::Data { - txs: res.block.data, - }), - evidence: Some(proto::tendermint::types::EvidenceList { - evidence: res - .block - .evidence - .into_vec() - .iter() - .map(|e| Ok(proto::tendermint::types::Evidence { - sum: Some( match e { - tendermint::evidence::Evidence::DuplicateVote(e) => { - let e2 = tendermint_proto::types::DuplicateVoteEvidence::from(e.deref().clone()); - proto::tendermint::types::evidence::Sum::DuplicateVoteEvidence(proto::tendermint::types::DuplicateVoteEvidence{ - vote_a: Some(proto::tendermint::types::Vote{ - r#type: match e.votes().0.vote_type { - tendermint::vote::Type::Prevote => proto::tendermint::types::SignedMsgType::Prevote as i32, - tendermint::vote::Type::Precommit => proto::tendermint::types::SignedMsgType::Precommit as i32, - }, - height: e.votes().0.height.into(), - round: e.votes().0.round.into(), - block_id: Some(proto::tendermint::types::BlockId{ - hash: e.votes().0.block_id.expect("block id").hash.into(), - part_set_header: Some(proto::tendermint::types::PartSetHeader{ - total: e.votes().0.block_id.expect("block id").part_set_header.total, - hash: e.votes().0.block_id.expect("block id").part_set_header.hash.into(), - }), - }), - timestamp: Some(pbjson_types::Timestamp{ - seconds: DateTime::parse_from_rfc3339(&e.votes().0.timestamp.expect("timestamp").to_rfc3339()).expect("timestamp should roundtrip to string").timestamp(), - nanos: DateTime::parse_from_rfc3339(&e.votes().0.timestamp.expect("timestamp").to_rfc3339()).expect("timestamp should roundtrip to string").timestamp_nanos_opt().ok_or_else(|| tonic::Status::invalid_argument("missing timestamp nanos"))? as i32, - }), - validator_address: e.votes().0.validator_address.into(), - validator_index: e.votes().0.validator_index.into(), - signature: e.votes().0.signature.clone().expect("signed vote").into(), - }), - vote_b: Some(proto::tendermint::types::Vote{ - r#type: match e.votes().1.vote_type { - tendermint::vote::Type::Prevote => proto::tendermint::types::SignedMsgType::Prevote as i32, - tendermint::vote::Type::Precommit => proto::tendermint::types::SignedMsgType::Precommit as i32, - }, - height: e.votes().1.height.into(), - round: e.votes().1.round.into(), - block_id: Some(proto::tendermint::types::BlockId{ - hash: e.votes().1.block_id.expect("block id").hash.into(), - part_set_header: Some(proto::tendermint::types::PartSetHeader{ - total: e.votes().1.block_id.expect("block id").part_set_header.total, - hash: e.votes().1.block_id.expect("block id").part_set_header.hash.into(), - }), - }), - timestamp: Some(pbjson_types::Timestamp{ - seconds: DateTime::parse_from_rfc3339(&e.votes().1.timestamp.expect("timestamp").to_rfc3339()).expect("timestamp should roundtrip to string").timestamp(), - nanos: DateTime::parse_from_rfc3339(&e.votes().1.timestamp.expect("timestamp").to_rfc3339()).expect("timestamp should roundtrip to string").timestamp_nanos_opt().ok_or_else(|| tonic::Status::invalid_argument("missing timestamp nanos"))? as i32, - }), - validator_address: e.votes().1.validator_address.into(), - validator_index: e.votes().1.validator_index.into(), - signature: e.votes().1.signature.clone().expect("signed vote").into(), - }), - total_voting_power: e2.total_voting_power, - validator_power: e2.validator_power, - timestamp: e2.timestamp.map(|t| pbjson_types::Timestamp{seconds: t.seconds, nanos: t.nanos}), - }) - }, - tendermint::evidence::Evidence::LightClientAttack(e) => { - let e2 = tendermint_proto::types::LightClientAttackEvidence::from(e.deref().clone()); - let e2_bytes = e2.encode_to_vec(); - let e3 = proto::tendermint::types::LightClientAttackEvidence::decode(e2_bytes.as_slice()).expect("can decode encoded data"); - proto::tendermint::types::evidence::Sum::LightClientAttackEvidence( - e3 - ) - } + // Parse the height from the inbound client request. + let GetBlockByHeightRequest { height } = req.into_inner(); + let height = + tendermint::block::Height::try_from(height).expect("height should be less than 2^63"); - }), - })) - .collect::, tonic::Status>>()?, - }), - last_commit: Some(proto::tendermint::types::Commit { - height: res.block.last_commit.as_ref().expect("last_commit").height.into(), - round: res.block.last_commit.as_ref().expect("last_commit").round.into(), - block_id: Some(proto::tendermint::types::BlockId { - hash: res.block.last_commit.as_ref().expect("last_commit").block_id.hash.into(), - part_set_header: Some(proto::tendermint::types::PartSetHeader { - total: res.block.last_commit.as_ref().expect("last_commit").block_id.part_set_header.total, - hash: res.block.last_commit.as_ref().expect("last_commit").block_id.part_set_header.hash.into(), - }), - }), - signatures: match res.block.last_commit { - Some(commit) => commit - .signatures - .into_iter() - .map(|s| Ok({ - match s { - tendermint::block::CommitSig::BlockIdFlagAbsent => proto::tendermint::types::CommitSig { - block_id_flag: proto::tendermint::types::BlockIdFlag::Absent as i32, - // No validator address, or timestamp is recorded for this variant. Not sure if this is a bug in tendermint-rs or not. - validator_address: vec![], - timestamp: None, - signature: vec![], - }, - tendermint::block::CommitSig::BlockIdFlagCommit { validator_address, timestamp, signature } => proto::tendermint::types::CommitSig { - block_id_flag: proto::tendermint::types::BlockIdFlag::Commit as i32, - validator_address: validator_address.into(), - timestamp: Some(pbjson_types::Timestamp{ - seconds: DateTime::parse_from_rfc3339(×tamp.to_rfc3339()).expect("timestamp should roundtrip to string").timestamp(), - nanos: DateTime::parse_from_rfc3339(×tamp.to_rfc3339()).expect("timestamp should roundtrip to string").timestamp_nanos_opt().ok_or_else(|| tonic::Status::invalid_argument("missing timestamp nanos"))? as i32, - }), - signature: signature.expect("signature").into(), - }, - tendermint::block::CommitSig::BlockIdFlagNil { validator_address, timestamp, signature } => proto::tendermint::types::CommitSig { - block_id_flag: proto::tendermint::types::BlockIdFlag::Nil as i32, - validator_address: validator_address.into(), - timestamp: Some(pbjson_types::Timestamp{ - seconds: DateTime::parse_from_rfc3339(×tamp.to_rfc3339()).expect("timestamp should roundtrip to string").timestamp(), - nanos: DateTime::parse_from_rfc3339(×tamp.to_rfc3339()).expect("timestamp should roundtrip to string").timestamp_nanos_opt().ok_or_else(|| tonic::Status::invalid_argument("missing timestamp nanos"))? as i32, - }), - signature: signature.expect("signature").into(), - }, - } - })) - .collect::, tonic::Status>>()?, - None => vec![], - }, - }), - }), - })) + // Fetch the block and forward Tendermint's response back to the client. + client + .block(height) + .await + .map_err(|e| tonic::Status::unavailable(format!("error querying abci: {e}"))) + .and_then(GetBlockByHeightResponse::try_from) + .map(tonic::Response::new) } }