From e15809e802b1c08fc534e054d0404caca136d896 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Wed, 14 Feb 2024 09:57:55 -0500 Subject: [PATCH] =?UTF-8?q?mock-consensus:=20=F0=9F=9A=80=20test=20node=20?= =?UTF-8?q?can=20send=20an=20empty=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #3792. see #3588. * `block` module defining interfaces to build a tendermint block, holding a unique reference to the `TestNode`. * `abci` module defining `TestNode` interfaces that will send consensus requests to the application. this includes: * BeginBlock * DeliverTx * EndBlock * Commit * `send_block` module defining a `TestNode` interface to send the requisite abci requests, given a `tendermint::Block`. * documentation is added for assorted public interfaces. this represents a huge, exciting step for work on the mock engine! we can now initialize and send an (empty) block to the consensus service. ✨ 🎊 ✨ 🎊 ✨ what next? this isn't a _comprehensive_ set of interfaces. `penumbra_mock_consensus::block::Builder` will certainly grow more methods as it is iterated upon. we'll use work in porting tests (see #3788) to drive which other fields are needed. forthcoming work will build upon this to: * introduce more builder methods to set other `Header` fields (e.g. timestamp, see #3759) * use the reference to the test node to set other `Header` fields (e.g. height) todo comments are left to that effect. --- * #3588 * #3792 --- crates/core/app/tests/mock_consensus.rs | 33 ++++ crates/test/mock-consensus/src/abci.rs | 167 +++++++++++++++++++ crates/test/mock-consensus/src/block.rs | 108 +++++++++++- crates/test/mock-consensus/src/lib.rs | 18 +- crates/test/mock-consensus/src/send_block.rs | 55 ++++++ 5 files changed, 374 insertions(+), 7 deletions(-) create mode 100644 crates/test/mock-consensus/src/abci.rs create mode 100644 crates/test/mock-consensus/src/send_block.rs diff --git a/crates/core/app/tests/mock_consensus.rs b/crates/core/app/tests/mock_consensus.rs index c88cef54cd..fa629336b8 100644 --- a/crates/core/app/tests/mock_consensus.rs +++ b/crates/core/app/tests/mock_consensus.rs @@ -9,6 +9,7 @@ use cnidarium::TempStorage; use common::BuilderExt; use penumbra_app::server::consensus::Consensus; use penumbra_genesis::AppState; +use tendermint::evidence::List; #[tokio::test] async fn mock_consensus_can_send_an_init_chain_request() -> anyhow::Result<()> { @@ -36,3 +37,35 @@ async fn mock_consensus_can_send_an_init_chain_request() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn mock_consensus_can_send_a_single_empty_block() -> anyhow::Result<()> { + // Install a test logger, and acquire some temporary storage. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + + // Instantiate the consensus service, and start the test node. + let mut engine = { + use penumbra_mock_consensus::TestNode; + let app_state = AppState::default(); + let consensus = Consensus::new(storage.as_ref().clone()); + TestNode::builder() + .single_validator() + .with_penumbra_auto_app_state(app_state)? + .init_chain(consensus) + .await? + }; + + let block = engine + .block() + .with_data(vec![]) + .with_evidence(List::new(Vec::new())) + .finish()?; + engine.send_block(block).await?; + + // Free our temporary storage. + drop(storage); + drop(guard); + + Ok(()) +} diff --git a/crates/test/mock-consensus/src/abci.rs b/crates/test/mock-consensus/src/abci.rs new file mode 100644 index 0000000000..b0f2f7cb6e --- /dev/null +++ b/crates/test/mock-consensus/src/abci.rs @@ -0,0 +1,167 @@ +//! [`TestNode`] interfaces for sending consensus requests to an ABCI application. + +use { + super::TestNode, + anyhow::anyhow, + bytes::Bytes, + tap::{Tap, TapFallible}, + tendermint::{ + abci::types::CommitInfo, + block::{Header, Round}, + v0_37::abci::{request, response, ConsensusRequest, ConsensusResponse}, + }, + tower::{BoxError, Service}, + tracing::{error, instrument, trace}, +}; + +/// ABCI-related interfaces. +impl TestNode +where + C: Service + + Send + + Clone + + 'static, + C::Future: Send + 'static, + C::Error: Sized, +{ + /// Yields a mutable reference to the consensus service when it is ready to accept a request. + async fn service(&mut self) -> Result<&mut C, anyhow::Error> { + use tower::ServiceExt; + self.consensus + .ready() + .tap(|_| trace!("waiting for consensus service")) + .await + .tap_err(|error| error!(?error, "failed waiting for consensus service")) + .map_err(|_| anyhow!("failed waiting for consensus service")) + .tap_ok(|_| trace!("consensus service is now ready")) + } + + /// Sends a [`ConsensusRequest::BeginBlock`] request to the ABCI application. + #[instrument(level = "debug", skip_all)] + pub async fn begin_block( + &mut self, + header: Header, + ) -> Result { + let request = ConsensusRequest::BeginBlock(request::BeginBlock { + hash: tendermint::Hash::None, + header, + last_commit_info: CommitInfo { + round: Round::from(1_u8), + votes: Default::default(), + }, + byzantine_validators: Default::default(), + }); + let service = self.service().await?; + match service + .tap(|_| trace!("sending BeginBlock request")) + .call(request) + .await + .tap_err(|error| error!(?error, "consensus service returned error")) + .map_err(|_| anyhow!("consensus service returned error"))? + { + ConsensusResponse::BeginBlock(response) => { + let response::BeginBlock { events } = &response; + trace!(?events, "received BeginBlock events"); + Ok(response) + } + response => { + error!(?response, "unexpected InitChain response"); + Err(anyhow!("unexpected InitChain response")) + } + } + } + + /// Sends a [`ConsensusRequest::DeliverTx`] request to the ABCI application. + #[instrument(level = "debug", skip_all)] + pub async fn deliver_tx(&mut self, tx: Bytes) -> Result { + let request = ConsensusRequest::DeliverTx(request::DeliverTx { tx }); + let service = self.service().await?; + match service + .tap(|_| trace!("sending DeliverTx request")) + .call(request) + .await + .tap_err(|error| error!(?error, "consensus service returned error")) + .map_err(|_| anyhow!("consensus service returned error"))? + { + ConsensusResponse::DeliverTx(response) => { + let response::DeliverTx { + code, + gas_used, + gas_wanted, + events, + .. + } = &response; + trace!( + ?code, + ?gas_used, + ?gas_wanted, + ?events, + "received DeliverTx response" + ); + Ok(response) + } + response => { + error!(?response, "unexpected DeliverTx response"); + Err(anyhow!("unexpected DeliverTx response")) + } + } + } + + /// Sends a [`ConsensusRequest::EndBlock`] request to the ABCI application. + #[instrument(level = "debug", skip_all)] + pub async fn end_block(&mut self) -> Result { + let request = ConsensusRequest::EndBlock(request::EndBlock { height: 1 }); + let service = self.service().await?; + match service + .call(request) + .await + .tap_err(|error| error!(?error, "consensus service returned error")) + .map_err(|_| anyhow!("consensus service returned error"))? + { + ConsensusResponse::EndBlock(response) => { + let response::EndBlock { + validator_updates, + consensus_param_updates, + events, + } = &response; + trace!( + ?validator_updates, + ?consensus_param_updates, + ?events, + "received EndBlock response" + ); + Ok(response) + } + response => { + error!(?response, "unexpected EndBlock response"); + Err(anyhow!("unexpected EndBlock response")) + } + } + } + + /// Sends a [`ConsensusRequest::Commit`] request to the ABCI application. + #[instrument(level = "debug", skip_all)] + pub async fn commit(&mut self) -> Result { + let request = ConsensusRequest::Commit; + let service = self.service().await?; + match service + .call(request) + .await + .tap_err(|error| error!(?error, "consensus service returned error")) + .map_err(|_| anyhow!("consensus service returned error"))? + { + ConsensusResponse::Commit(response) => { + let response::Commit { + data, + retain_height, + } = &response; + trace!(?data, ?retain_height, "received Commit response"); + Ok(response) + } + response => { + error!(?response, "unexpected Commit response"); + Err(anyhow!("unexpected Commit response")) + } + } + } +} diff --git a/crates/test/mock-consensus/src/block.rs b/crates/test/mock-consensus/src/block.rs index 96d14e9e2a..5fc880caff 100644 --- a/crates/test/mock-consensus/src/block.rs +++ b/crates/test/mock-consensus/src/block.rs @@ -1,7 +1,107 @@ -// TODO: see #3792. +//! [`Builder`] facilities for constructing [`Block`]s. +//! +/// Builders are acquired by calling [`TestNode::block()`]. +use { + crate::TestNode, + anyhow::bail, + tendermint::{ + account, + block::{header::Version, Block, Commit, Header, Height}, + chain, evidence, AppHash, Hash, + }, +}; -use crate::TestNode; +/// A builder, used to prepare and instantiate a new [`Block`]. +/// +/// These are acquired by calling [`TestNode::block()`]. +pub struct Builder<'e, C> { + /// A unique reference to the test node. + // + // NB: this is currently unused, but will eventually be used to fill in header fields, etc. + #[allow(dead_code)] + test_node: &'e mut TestNode, -struct _Builder<'e, C> { - engine: &'e mut TestNode, + /// Transaction data. + data: Option>>, + + /// Evidence of malfeasance. + evidence: Option, + + /// Last commit. + last_commit: Option, +} + +impl TestNode { + /// Returns a new [`Builder`]. + pub fn block<'e>(&'e mut self) -> Builder<'e, C> { + Builder { + test_node: self, + data: Default::default(), + evidence: Default::default(), + last_commit: Default::default(), + } + } +} + +impl<'e, C> Builder<'e, C> { + /// Sets the data for this block. + pub fn with_data(self, data: Vec>) -> Self { + Self { + data: Some(data), + ..self + } + } + + /// Sets the evidence [`List`][evidence::List] for this block. + pub fn with_evidence(self, evidence: evidence::List) -> Self { + Self { + evidence: Some(evidence), + ..self + } + } + + /// Sets the last [`Commit`] for this block. + pub fn with_last_commit(self, last_commit: Commit) -> Self { + Self { + last_commit: Some(last_commit), + ..self + } + } + + // TODO(kate): add more `with_` setters for fields in the header. + // TODO(kate): set some fields using state in the test node. + + /// Consumes this builder, returning a [`Block`]. + pub fn finish(self) -> Result { + let Self { + data: Some(data), + evidence: Some(evidence), + last_commit, + test_node: _, + } = self + else { + bail!("builder was not fully initialized") + }; + + let header = Header { + version: Version { block: 1, app: 1 }, + chain_id: chain::Id::try_from("test".to_owned())?, + height: Height::try_from(1_u8)?, + time: tendermint::Time::now(), + last_block_id: None, + last_commit_hash: None, + data_hash: None, + validators_hash: Hash::None, + next_validators_hash: Hash::None, + consensus_hash: Hash::None, + app_hash: AppHash::try_from(Vec::default())?, + last_results_hash: None, + evidence_hash: None, + proposer_address: account::Id::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + }; + + Block::new(header, data, evidence, last_commit).map_err(Into::into) + } } diff --git a/crates/test/mock-consensus/src/lib.rs b/crates/test/mock-consensus/src/lib.rs index c607d0bfbb..fbfd62a0a7 100644 --- a/crates/test/mock-consensus/src/lib.rs +++ b/crates/test/mock-consensus/src/lib.rs @@ -1,12 +1,24 @@ //! `penumbra-mock-consensus` is a library for testing consensus-driven applications. +//! +//! See [`TestNode`] for more information. // // see penumbra-zone/penumbra#3588. -mod block; pub mod builder; -// TODO(kate): this is a temporary allowance while we set the test node up. -#[allow(dead_code)] +mod abci; +mod block; +mod send_block; + +/// A test node. +/// +/// Construct a new test node by calling [`TestNode::builder()`]. Use [`TestNode::block()`] to +/// build a new [`Block`]. +/// +/// This contains a consensus service `C`, which should be a [`tower::Service`] implementor that +/// accepts [`ConsensusRequest`][0_37::abci::ConsensusRequest]s, and returns +/// [`ConsensusResponse`][0_37::abci::ConsensusResponse]s. For `tower-abci` users, this should +/// correspond with the `ConsensusService` parameter of the `Server` type. pub struct TestNode { consensus: C, last_app_hash: Vec, diff --git a/crates/test/mock-consensus/src/send_block.rs b/crates/test/mock-consensus/src/send_block.rs new file mode 100644 index 0000000000..e18a220a21 --- /dev/null +++ b/crates/test/mock-consensus/src/send_block.rs @@ -0,0 +1,55 @@ +//! [`TestNode`] interfaces for sending [`Block`]s. + +use { + crate::TestNode, + tendermint::{ + v0_37::abci::{ConsensusRequest, ConsensusResponse}, + Block, + }, + tower::{BoxError, Service}, + tracing::{info, instrument}, +}; + +impl TestNode +where + C: Service + + Send + + Clone + + 'static, + C::Future: Send + 'static, + C::Error: Sized, +{ + /// Sends the provided [`Block`] to the consensus service. + /// + /// Use [`TestNode::block()`] to build a new block. + #[instrument( + level = "info", + skip_all, + fields( + height = %block.header.height, + time = %block.header.time, + app_hash = %block.header.app_hash, + ) + )] + pub async fn send_block(&mut self, block: Block) -> Result<(), anyhow::Error> { + let Block { + header, + data, + evidence: _, + last_commit: _, + .. + } = block; + + info!("sending block"); + self.begin_block(header).await?; + for tx in data { + let tx = tx.into(); + self.deliver_tx(tx).await?; + } + self.end_block().await?; + self.commit().await?; + info!("finished sending block"); + + Ok(()) + } +}