Skip to content

Commit

Permalink
mock-tendermint-proxy: 🪩 define tendermint proxy mocks
Browse files Browse the repository at this point in the history
our broader goal is to wire up plumbing so that a test node can handle
the tendermint proxy service client's requests sent by the view server
when planning transactions.

as a step towards that, add a new crate to the workspace. for the time
being, we provide an empty `Stub` proxy. we will build upon this and
sort out connecting it to a running `TestNode<C>` next.
  • Loading branch information
cratelyn committed May 30, 2024
1 parent 33b6a53 commit 2a8859b
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ members = [
"crates/proto",
"crates/test/mock-client",
"crates/test/mock-consensus",
"crates/test/mock-tendermint-proxy",
"crates/test/tct-property-test",
"crates/test/tracing-subscriber",
"crates/util/auto-https",
Expand Down Expand Up @@ -178,6 +179,7 @@ penumbra-ibc = { default-features = false, path = "crates/co
penumbra-keys = { default-features = false, path = "crates/core/keys" }
penumbra-mock-client = { path = "crates/test/mock-client" }
penumbra-mock-consensus = { path = "crates/test/mock-consensus" }
penumbra-mock-tendermint-proxy = { path = "crates/test/mock-tendermint-proxy" }
penumbra-num = { default-features = false, path = "crates/core/num" }
penumbra-proof-params = { default-features = false, path = "crates/crypto/proof-params" }
penumbra-proof-setup = { path = "crates/crypto/proof-setup" }
Expand Down
16 changes: 16 additions & 0 deletions crates/test/mock-tendermint-proxy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "penumbra-mock-tendermint-proxy"
authors.workspace = true
edition.workspace = true
version.workspace = true
repository.workspace = true
homepage.workspace = true
license.workspace = true

[dependencies]
penumbra-mock-consensus = { workspace = true }
penumbra-proto = { workspace = true, features = ["rpc", "tendermint"] }
tap = { workspace = true }
tendermint = { workspace = true }
tonic = { workspace = true }
tracing = { workspace = true }
6 changes: 6 additions & 0 deletions crates/test/mock-tendermint-proxy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! [`TendermintProxyService`] implementations for use in [`penumbra-mock-consensus`] tests.
mod proxy;
mod stub;

pub use crate::{proxy::TestNodeProxy, stub::StubProxy};
205 changes: 205 additions & 0 deletions crates/test/mock-tendermint-proxy/src/proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
use {
penumbra_proto::{
tendermint::p2p::DefaultNodeInfo,
util::tendermint_proxy::v1::{
tendermint_proxy_service_server::TendermintProxyService, AbciQueryRequest,
AbciQueryResponse, BroadcastTxAsyncRequest, BroadcastTxAsyncResponse,
BroadcastTxSyncRequest, BroadcastTxSyncResponse, GetBlockByHeightRequest,
GetBlockByHeightResponse, GetStatusRequest, GetStatusResponse, GetTxRequest,
GetTxResponse, SyncInfo,
},
},
std::{
collections::BTreeMap,
sync::{Arc, RwLock},
},
tap::{Tap, TapFallible, TapOptional},
tendermint::block::{Block, Height},
tonic::Status,
tracing::instrument,
};

/// A tendermint proxy service for use in tests.
///
/// This type implements [`TendermintProxyService`], but can be configured to report the blocks
/// generated by a [`penumbra_mock_consensus::TestNode`].
#[derive(Default)]
pub struct TestNodeProxy {
inner: Arc<Inner>,
}

#[derive(Default)]
struct Inner {
/// A map of the [`Blocks`] that have been seen so far, keyed by [`Height`].
blocks: RwLock<BTreeMap<Height, Block>>,
}

impl TestNodeProxy {
/// Creates a new [`TestNodeProxy`].
pub fn new<C>() -> Self {
Default::default()
}

/// Returns a boxed function that will add [`Blocks`] to this proxy.
pub fn on_block_callback(&self) -> penumbra_mock_consensus::OnBlockFn {
// Create a new reference to the shared map of blocks we've seen.
let Self { inner } = self;
let inner = Arc::clone(inner);

Box::new(move |block| inner.on_block(block))
}

/// Returns the latest block height.
fn latest_block_height(&self) -> tendermint::block::Height {
self.inner
.blocks()
.last_key_value()
.map(|(height, _)| *height)
.expect("blocks should not be empty")
}
}

impl Inner {
#[instrument(level = "debug", skip_all)]
fn on_block(&self, block: tendermint::Block) {
// Add this block to the proxy's book-keeping.
let height = block.header.height;
self.blocks_mut()
.insert(height, block)
.map(|_overwritten| {
// ...or panic if we have been given block with duplicate heights.
panic!("proxy received two blocks with height {height}");
})
.tap_none(|| {
tracing::debug!(?height, "received block");
});
}

/// Acquires a write-lock on the map of blocks we have seen before.
fn blocks(&self) -> std::sync::RwLockReadGuard<'_, BTreeMap<Height, Block>> {
let Self { blocks } = self;
blocks
.tap(|_| tracing::trace!("acquiring read lock"))
.read()
.tap(|_| tracing::trace!("acquired read lock"))
.tap_err(|_| tracing::error!("failed to acquire read lock"))
.expect("block lock should never be poisoned")
}

/// Acquires a write-lock on the map of blocks we have seen before.
fn blocks_mut(&self) -> std::sync::RwLockWriteGuard<'_, BTreeMap<Height, Block>> {
let Self { blocks } = self;
blocks
.tap(|_| tracing::trace!("acquiring write lock"))
.write()
.tap(|_| tracing::trace!("acquired write lock"))
.tap_err(|_| tracing::error!("failed to acquire write lock"))
.expect("block lock should never be poisoned")
}
}

#[tonic::async_trait]
impl TendermintProxyService for TestNodeProxy {
async fn get_tx(
&self,
_req: tonic::Request<GetTxRequest>,
) -> Result<tonic::Response<GetTxResponse>, Status> {
Err(Status::unimplemented("get_tx"))
}

/// Broadcasts a transaction asynchronously.
#[instrument(
level = "info",
skip_all,
fields(req_id = tracing::field::Empty),
)]
async fn broadcast_tx_async(
&self,
_req: tonic::Request<BroadcastTxAsyncRequest>,
) -> Result<tonic::Response<BroadcastTxAsyncResponse>, Status> {
Ok(tonic::Response::new(BroadcastTxAsyncResponse {
code: 0,
data: Vec::default(),
log: String::default(),
hash: Vec::default(),
}))
}

// Broadcasts a transaction synchronously.
#[instrument(
level = "info",
skip_all,
fields(req_id = tracing::field::Empty),
)]
async fn broadcast_tx_sync(
&self,
_req: tonic::Request<BroadcastTxSyncRequest>,
) -> Result<tonic::Response<BroadcastTxSyncResponse>, Status> {
Ok(tonic::Response::new(BroadcastTxSyncResponse {
code: 0,
data: Vec::default(),
log: String::default(),
hash: Vec::default(),
}))
}

// Queries the current status.
#[instrument(level = "info", skip_all)]
async fn get_status(
&self,
req: tonic::Request<GetStatusRequest>,
) -> Result<tonic::Response<GetStatusResponse>, Status> {
let GetStatusRequest { .. } = req.into_inner();
let latest_block_height = self.latest_block_height().into();

let sync_info = SyncInfo {
latest_block_hash: vec![],
latest_app_hash: vec![],
latest_block_height,
latest_block_time: None,
// Tests run with a single node, so it is never catching up.
catching_up: false,
};

Ok(GetStatusResponse {
node_info: Some(DefaultNodeInfo::default()),
sync_info: Some(sync_info),
validator_info: Some(Default::default()),
})
.map(tonic::Response::new)
}

#[instrument(level = "info", skip_all)]
async fn abci_query(
&self,
_req: tonic::Request<AbciQueryRequest>,
) -> Result<tonic::Response<AbciQueryResponse>, Status> {
Err(Status::unimplemented("abci_query"))
}

#[instrument(level = "info", skip_all)]
async fn get_block_by_height(
&self,
req: tonic::Request<GetBlockByHeightRequest>,
) -> Result<tonic::Response<GetBlockByHeightResponse>, Status> {
// 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");

let block = self
.inner
.blocks()
.get(&height)
.cloned()
.map(penumbra_proto::tendermint::types::Block::try_from)
.transpose()?;
let block_id = block
.as_ref() // is this off-by-one? should we be getting the id of the last commit?
.and_then(|b| b.last_commit.as_ref())
.and_then(|c| c.block_id.as_ref())
.cloned();

Ok(GetBlockByHeightResponse { block_id, block }).map(tonic::Response::new)
}
}
77 changes: 77 additions & 0 deletions crates/test/mock-tendermint-proxy/src/stub.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use {
penumbra_proto::util::tendermint_proxy::v1::{
tendermint_proxy_service_server::TendermintProxyService, AbciQueryRequest,
AbciQueryResponse, BroadcastTxAsyncRequest, BroadcastTxAsyncResponse,
BroadcastTxSyncRequest, BroadcastTxSyncResponse, GetBlockByHeightRequest,
GetBlockByHeightResponse, GetStatusRequest, GetStatusResponse, GetTxRequest, GetTxResponse,
},
tonic::Status,
tracing::instrument,
};

/// A tendermint proxy service for use in tests.
///
/// This implements [`TendermintProxyService`], but will return a [`Status::unimplemented`] error
/// for any requests it receives.
pub struct StubProxy;

#[tonic::async_trait]
impl TendermintProxyService for StubProxy {
async fn get_tx(
&self,
_req: tonic::Request<GetTxRequest>,
) -> Result<tonic::Response<GetTxResponse>, Status> {
Err(Status::unimplemented("get_tx"))
}

/// Broadcasts a transaction asynchronously.
#[instrument(
level = "info",
skip_all,
fields(req_id = tracing::field::Empty),
)]
async fn broadcast_tx_async(
&self,
_req: tonic::Request<BroadcastTxAsyncRequest>,
) -> Result<tonic::Response<BroadcastTxAsyncResponse>, Status> {
Err(Status::unimplemented("broadcast_tx_async"))
}

// Broadcasts a transaction synchronously.
#[instrument(
level = "info",
skip_all,
fields(req_id = tracing::field::Empty),
)]
async fn broadcast_tx_sync(
&self,
_req: tonic::Request<BroadcastTxSyncRequest>,
) -> Result<tonic::Response<BroadcastTxSyncResponse>, Status> {
Err(Status::unimplemented("broadcast_tx_sync"))
}

// Queries the current status.
#[instrument(level = "info", skip_all)]
async fn get_status(
&self,
__req: tonic::Request<GetStatusRequest>,
) -> Result<tonic::Response<GetStatusResponse>, Status> {
Err(Status::unimplemented("get_status"))
}

#[instrument(level = "info", skip_all)]
async fn abci_query(
&self,
_req: tonic::Request<AbciQueryRequest>,
) -> Result<tonic::Response<AbciQueryResponse>, Status> {
Err(Status::unimplemented("abci_query"))
}

#[instrument(level = "info", skip_all)]
async fn get_block_by_height(
&self,
_req: tonic::Request<GetBlockByHeightRequest>,
) -> Result<tonic::Response<GetBlockByHeightResponse>, Status> {
Err(Status::unimplemented("get_block_by_height"))
}
}
1 change: 1 addition & 0 deletions deployments/scripts/rust-docs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ cargo +nightly doc --no-deps \
-p penumbra-keys \
-p penumbra-measure \
-p penumbra-mock-consensus \
-p penumbra-mock-tendermint-proxy \
-p penumbra-mock-client \
-p penumbra-num \
-p penumbra-proof-params \
Expand Down

0 comments on commit 2a8859b

Please sign in to comment.