diff --git a/Cargo.lock b/Cargo.lock index fc532cb70b..89138cc86f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4682,6 +4682,7 @@ dependencies = [ "tendermint-light-client-verifier", "tendermint-proto", "tokio", + "tokio-util 0.7.10", "tonic", "tower", "tower-abci", @@ -5215,6 +5216,14 @@ dependencies = [ [[package]] name = "penumbra-mock-consensus" version = "0.67.0" +dependencies = [ + "anyhow", + "bytes", + "tap", + "tendermint", + "tower", + "tracing", +] [[package]] name = "penumbra-num" diff --git a/Cargo.toml b/Cargo.toml index 82c360f594..f8eb26964a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -226,5 +226,5 @@ tower = {version = "0.4.0"} tower-http = {version = "0.4"} tower-service = {version = "0.3.2"} tracing = {version = "0.1"} -tracing-subscriber = {version = "0.3.17"} +tracing-subscriber = {version = "0.3.17", features = ["env-filter"]} url = {version = "2.2"} diff --git a/crates/cnidarium/src/storage/temp.rs b/crates/cnidarium/src/storage/temp.rs index 0657f3783b..e82368c899 100644 --- a/crates/cnidarium/src/storage/temp.rs +++ b/crates/cnidarium/src/storage/temp.rs @@ -18,6 +18,12 @@ impl Deref for TempStorage { } } +impl AsRef for TempStorage { + fn as_ref(&self) -> &Storage { + &self.inner + } +} + impl TempStorage { pub async fn new() -> anyhow::Result { let dir = tempfile::tempdir()?; diff --git a/crates/core/app/Cargo.toml b/crates/core/app/Cargo.toml index aadfe1467c..adbbd37f15 100644 --- a/crates/core/app/Cargo.toml +++ b/crates/core/app/Cargo.toml @@ -41,6 +41,7 @@ decaf377 = {workspace = true, default-features = true} decaf377-rdsa = {workspace = true} jmt = {workspace = true} tokio = {workspace = true, features = ["full", "tracing"]} +tokio-util = {workspace = true} async-trait = {workspace = true} tonic = {workspace = true} futures = {workspace = true} diff --git a/crates/core/app/src/server.rs b/crates/core/app/src/server.rs index fd99a93bec..c151f5ba64 100644 --- a/crates/core/app/src/server.rs +++ b/crates/core/app/src/server.rs @@ -50,10 +50,7 @@ pub fn new( req.create_span() })) .layer(EventIndexLayer::index_all()) - .service(tower_actor::Actor::new(10, |queue: _| { - let storage = storage.clone(); - async move { Consensus::new(storage.clone(), queue).await?.run().await } - })); + .service(Consensus::new(storage.clone())); let mempool = tower::ServiceBuilder::new() .layer(request_span::layer(|req: &MempoolRequest| { use penumbra_tower_trace::v037::RequestExt; diff --git a/crates/core/app/src/server/consensus.rs b/crates/core/app/src/server/consensus.rs index fbd5d62e03..d15164ff0b 100644 --- a/crates/core/app/src/server/consensus.rs +++ b/crates/core/app/src/server/consensus.rs @@ -6,6 +6,7 @@ use tendermint::v0_37::abci::{ request, response, ConsensusRequest as Request, ConsensusResponse as Response, }; use tokio::sync::mpsc; +use tower::BoxError; use tower_actor::Message; use tracing::Instrument; @@ -29,7 +30,21 @@ fn trace_events(events: &[Event]) { } impl Consensus { - pub async fn new( + const QUEUE_SIZE: usize = 10; + + pub fn new(storage: Storage) -> tower_actor::Actor { + tower_actor::Actor::new(Self::QUEUE_SIZE, |queue: _| { + let storage = storage.clone(); + async move { + Consensus::new_inner(storage.clone(), queue) + .await? + .run() + .await + } + }) + } + + async fn new_inner( storage: Storage, queue: mpsc::Receiver>, ) -> Result { diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index 74df55887a..90955712a8 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -1,9 +1,32 @@ +//! Shared integration testing facilities. + +// NB: Allow dead code, these are in fact shared by files in `tests/`. +#![allow(dead_code)] + use async_trait::async_trait; use cnidarium::TempStorage; use penumbra_app::app::App; use penumbra_genesis::AppState; use std::ops::Deref; +// Installs a tracing subscriber to log events until the returned guard is dropped. +pub fn set_tracing_subscriber() -> tracing::subscriber::DefaultGuard { + use tracing_subscriber::filter::EnvFilter; + + let filter = "debug,penumbra_app=trace,penumbra_mock_consensus=trace"; + let filter = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new(filter)) + .expect("should have a valid filter directive"); + + let subscriber = tracing_subscriber::fmt() + .with_env_filter(filter) + .pretty() + .with_test_writer() + .finish(); + + tracing::subscriber::set_default(subscriber) +} + #[async_trait] pub trait TempStorageExt: Sized { async fn apply_genesis(self, genesis: AppState) -> anyhow::Result; diff --git a/crates/core/app/tests/mock_consensus.rs b/crates/core/app/tests/mock_consensus.rs index 2ca90f8277..7205d025fb 100644 --- a/crates/core/app/tests/mock_consensus.rs +++ b/crates/core/app/tests/mock_consensus.rs @@ -3,21 +3,32 @@ // Note: these should eventually replace the existing test cases. mock consensus tests are placed // here while the engine is still in development. See #3588. -use {cnidarium::TempStorage, penumbra_mock_consensus::TestNode}; +mod common; -#[tokio::test] -#[should_panic] -#[allow(unreachable_code, unused)] -async fn an_app_with_mock_consensus_can_be_instantiated() { - let storage = TempStorage::new().await.unwrap(); +use cnidarium::TempStorage; +use penumbra_app::server::consensus::Consensus; - // TODO(kate): bind this to an in-memory channel/writer instead. - let addr: std::net::SocketAddr = todo!(); - let abci_server = penumbra_app::server::new(todo!()).listen_tcp(addr); +#[tokio::test] +async fn mock_consensus_can_send_a_failing_init_chain_request() -> anyhow::Result<()> { + // Install a test logger, and acquire some temporary storage. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; - let _engine = TestNode::<()>::builder() + // Instantiate the consensus service, and start the test node. + use penumbra_mock_consensus::TestNode; + let consensus = Consensus::new(storage.as_ref().clone()); + let engine = TestNode::builder() .single_validator() .app_state(() /*genesis::AppState::default()*/) - .init_chain(abci_server) + .init_chain(consensus) .await; + + // NB: we don't expect this to succeed... yet. + assert!(engine.is_err(), "init_chain does not return an Ok(()) yet"); + + // Free our temporary storage. + drop(storage); + drop(guard); + + Ok(()) } diff --git a/crates/test/mock-consensus/Cargo.toml b/crates/test/mock-consensus/Cargo.toml index 52714cbcf7..ff43d37305 100644 --- a/crates/test/mock-consensus/Cargo.toml +++ b/crates/test/mock-consensus/Cargo.toml @@ -8,3 +8,9 @@ homepage.workspace = true license.workspace = true [dependencies] +anyhow = { workspace = true } +bytes = { workspace = true } +tendermint = { workspace = true } +tower = { workspace = true, features = ["full"] } +tracing = { workspace = true } +tap = "1.0.1" diff --git a/crates/test/mock-consensus/src/block.rs b/crates/test/mock-consensus/src/block.rs new file mode 100644 index 0000000000..96d14e9e2a --- /dev/null +++ b/crates/test/mock-consensus/src/block.rs @@ -0,0 +1,7 @@ +// TODO: see #3792. + +use crate::TestNode; + +struct _Builder<'e, C> { + engine: &'e mut TestNode, +} diff --git a/crates/test/mock-consensus/src/builder.rs b/crates/test/mock-consensus/src/builder.rs new file mode 100644 index 0000000000..a1063cb006 --- /dev/null +++ b/crates/test/mock-consensus/src/builder.rs @@ -0,0 +1,38 @@ +//! [`Builder`] interfaces, for creating new [`TestNode`]s. + +/// [`Builder`] interfaces for chain initialization. +/// +/// Most importantly, defines [`Builder::init_chain()`]. +mod init_chain; + +use crate::TestNode; + +/// A buider, used to prepare and instantiate a new [`TestNode`]. +pub struct Builder; + +impl TestNode<()> { + /// Returns a new [`Builder`]. + pub fn builder() -> Builder { + Builder + } +} + +impl Builder { + // TODO: add other convenience methods for validator config? + + /// Creates a single validator with a randomly generated key. + pub fn single_validator(self) -> Self { + // this does not do anything yet + self + } + + pub fn app_state(self, _: ()) -> Self { + // this does not do anything yet + self + } + + pub fn app_state_bytes(self, _: Vec) -> Self { + // this does not do anything yet + self + } +} diff --git a/crates/test/mock-consensus/src/builder/init_chain.rs b/crates/test/mock-consensus/src/builder/init_chain.rs new file mode 100644 index 0000000000..d59b340212 --- /dev/null +++ b/crates/test/mock-consensus/src/builder/init_chain.rs @@ -0,0 +1,94 @@ +use { + super::*, + anyhow::{anyhow, bail}, + std::time, + tap::TapFallible, + tendermint::{ + block, + consensus::{ + self, + params::{AbciParams, ValidatorParams, VersionParams}, + }, + evidence, + v0_37::abci::{ConsensusRequest, ConsensusResponse}, + }, + tower::{BoxError, Service, ServiceExt}, + tracing::{debug, error}, +}; + +impl Builder { + /// Consumes this builder, using the provided consensus service. + pub async fn init_chain(self, mut consensus: C) -> Result, anyhow::Error> + where + C: Service + + Send + + Clone + + 'static, + C::Future: Send + 'static, + C::Error: Sized, + { + use tendermint::v0_37::abci::response; + + let request = Self::init_chain_request(); + let service = consensus + .ready() + .await + .tap_err(|error| error!(?error, "failed waiting for consensus service")) + .map_err(|_| anyhow!("failed waiting for consensus service"))?; + + let response::InitChain { app_hash, .. } = match service + .call(request) + .await + .tap_ok(|resp| debug!(?resp, "received response from consensus service")) + .tap_err(|error| error!(?error, "consensus service returned error")) + .map_err(|_| anyhow!("consensus service returned error"))? + { + ConsensusResponse::InitChain(resp) => resp, + response => { + error!(?response, "unexpected InitChain response"); + bail!("unexpected InitChain response"); + } + }; + + Ok(TestNode { + consensus, + last_app_hash: app_hash.as_bytes().to_owned(), + }) + } + + fn init_chain_request() -> ConsensusRequest { + use tendermint::v0_37::abci::request::InitChain; + let consensus_params = Self::consensus_params(); + let app_state_bytes = bytes::Bytes::new(); + ConsensusRequest::InitChain(InitChain { + time: tendermint::Time::now(), + chain_id: "test".to_string(), // XXX const here? + consensus_params, + validators: vec![], + app_state_bytes, + initial_height: 1_u32.into(), + }) + } + + fn consensus_params() -> consensus::Params { + consensus::Params { + block: block::Size { + max_bytes: 1, + max_gas: 1, + time_iota_ms: 1, + }, + evidence: evidence::Params { + max_age_num_blocks: 1, + max_age_duration: evidence::Duration(time::Duration::from_secs(1)), + max_bytes: 1, + }, + validator: ValidatorParams { + pub_key_types: vec![], + }, + version: Some(VersionParams { app: 1 }), + abci: AbciParams { + vote_extensions_enable_height: None, + }, + } + } +} diff --git a/crates/test/mock-consensus/src/lib.rs b/crates/test/mock-consensus/src/lib.rs index 7db5427d1b..4e3faa8618 100644 --- a/crates/test/mock-consensus/src/lib.rs +++ b/crates/test/mock-consensus/src/lib.rs @@ -1,45 +1,19 @@ -pub struct TestNode { - #[allow(dead_code)] - abci_server: S, - _last_app_hash: Vec, +//! `penumbra-mock-consensus` is a library for testing consensus-driven applications. +// +// see penumbra-zone/penumbra#3588. + +mod block; +mod builder; + +// TODO(kate): this is a temporary allowance while we set the test node up. +#[allow(dead_code)] +pub struct TestNode { + consensus: C, + last_app_hash: Vec, } -pub mod block { - use crate::TestNode; - - struct _Builder<'e, C> { - engine: &'e mut TestNode, - } -} - -pub struct Builder; - -impl TestNode { - pub fn builder() -> Builder { - Builder - } -} - -impl Builder { - // TODO: add other convenience methods for validator config? - - /// Creates a single validator with a randomly generated key. - pub fn single_validator(self) -> Self { +impl TestNode { + pub fn next_block() -> tendermint::Block { todo!(); } - - pub fn app_state(self, _: ()) -> Self { - todo!() - } - - pub fn app_state_bytes(self, _: Vec) -> Self { - todo!() - } - - pub async fn init_chain(self, abci_server: S) -> TestNode { - TestNode { - abci_server, - _last_app_hash: vec![], - } - } }