Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mock-consensus: 🚀 test node can send an empty block #3822

Merged
merged 2 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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?
};
cratelyn marked this conversation as resolved.
Show resolved Hide resolved

let block = engine
.block()
.with_data(vec![])
.with_evidence(List::new(Vec::new()))
.finish()?;
engine.send_block(block).await?;
cratelyn marked this conversation as resolved.
Show resolved Hide resolved

// 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
Loading