Skip to content

Commit

Permalink
mock-consensus: 🚀 test node can send an empty block
Browse files Browse the repository at this point in the history
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
  • Loading branch information
cratelyn committed Feb 15, 2024
1 parent 7d61991 commit e15809e
Show file tree
Hide file tree
Showing 5 changed files with 374 additions and 7 deletions.
33 changes: 33 additions & 0 deletions crates/core/app/tests/mock_consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down Expand Up @@ -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(())
}
167 changes: 167 additions & 0 deletions crates/test/mock-consensus/src/abci.rs
Original file line number Diff line number Diff line change
@@ -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<C> TestNode<C>
where
C: Service<ConsensusRequest, Response = ConsensusResponse, Error = BoxError>
+ 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<response::BeginBlock, anyhow::Error> {
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<response::DeliverTx, anyhow::Error> {
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<response::EndBlock, anyhow::Error> {
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<response::Commit, anyhow::Error> {
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"))
}
}
}
}
108 changes: 104 additions & 4 deletions crates/test/mock-consensus/src/block.rs
Original file line number Diff line number Diff line change
@@ -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<C>,

struct _Builder<'e, C> {
engine: &'e mut TestNode<C>,
/// Transaction data.
data: Option<Vec<Vec<u8>>>,

/// Evidence of malfeasance.
evidence: Option<evidence::List>,

/// Last commit.
last_commit: Option<Commit>,
}

impl<C> TestNode<C> {
/// 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<Vec<u8>>) -> 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<Block, anyhow::Error> {
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)
}
}
18 changes: 15 additions & 3 deletions crates/test/mock-consensus/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<C> {
consensus: C,
last_app_hash: Vec<u8>,
Expand Down
Loading

0 comments on commit e15809e

Please sign in to comment.