From b6fe24f0a9c5e917d3f35cf1b5033e7e25fb7c23 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Thu, 11 Apr 2024 17:55:08 -0400 Subject: [PATCH] =?UTF-8?q?tests(app):=20=F0=9F=92=8E=20polish=20mock=20co?= =?UTF-8?q?nsensus=20test=20infrastructure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit see #3588. follows #4184 and #4181. this takes a pass through the shared, Penumbra-specific test infrastructure for mock consensus tests. notably, this decomposes `init_chain.rs`, which has now become somewhat redundant with the existence of other more involved tests of e.g. validator uptime tracking. this also cleans up some unused imports, guards against future occurrences of that issue (_sharing code in `tests/` files is awkward_), and decomposes the `common/mod.rs` file into some distinct standalone components. this also belatedly removes the `common::start_test_node()` helper. at some point (_i was unable to find the link_) it was suggested that we refrain from a shared setup helper like that. this branch removes that helper, and updates its call-sites. this branch is largely code motion, and is intended to be a last bit of cleanup as we prepare for #3588 to wind down. :heart: --------- Co-authored-by: Henry de Valence --- .../app_can_spend_notes_and_detect_outputs.rs | 19 ++- crates/core/app/tests/common/mod.rs | 139 ++---------------- .../core/app/tests/common/temp_storage_ext.rs | 33 +++++ .../app/tests/common/test_node_builder_ext.rs | 11 +- crates/core/app/tests/common/test_node_ext.rs | 45 ++++++ .../app/tests/common/tracing_subscriber.rs | 29 ++++ crates/core/app/tests/init_chain.rs | 85 ----------- ...onsensus_can_define_a_genesis_validator.rs | 58 ++++++++ ...sus_can_send_a_sequence_of_empty_blocks.rs | 43 ++++++ 9 files changed, 244 insertions(+), 218 deletions(-) create mode 100644 crates/core/app/tests/common/temp_storage_ext.rs create mode 100644 crates/core/app/tests/common/test_node_ext.rs create mode 100644 crates/core/app/tests/common/tracing_subscriber.rs delete mode 100644 crates/core/app/tests/init_chain.rs create mode 100644 crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs create mode 100644 crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs diff --git a/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs index 580145a74f..d4bf8577d1 100644 --- a/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs +++ b/crates/core/app/tests/app_can_spend_notes_and_detect_outputs.rs @@ -1,8 +1,8 @@ -mod common; - use { + self::common::BuilderExt, anyhow::anyhow, cnidarium::TempStorage, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, @@ -13,16 +13,27 @@ use { memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan, }, rand_core::OsRng, - tap::Tap, + tap::{Tap, TapFallible}, tracing::info, }; +mod common; + #[tokio::test] async fn app_can_spend_notes_and_detect_outputs() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. let guard = common::set_tracing_subscriber(); let storage = TempStorage::new().await?; - let mut test_node = common::start_test_node(&storage).await?; + let mut test_node = { + 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 + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; // Sync the mock client, using the test wallet's spend key, to the latest snapshot. let mut client = MockClient::new(test_keys::SPEND_KEY.clone()) diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index 043d3d39f0..b1b00df1a8 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -1,20 +1,10 @@ //! Shared integration testing facilities. -// NB: Allow dead code, and unused imports. these are shared and consumed by files in `tests/`. -#![allow(dead_code, unused_imports)] - -pub use self::test_node_builder_ext::BuilderExt; - -use { - async_trait::async_trait, - cnidarium::TempStorage, - penumbra_app::{ - app::App, - genesis::AppState, - server::consensus::{Consensus, ConsensusService}, - }, - penumbra_mock_consensus::TestNode, - std::ops::Deref, +// NB: these reƫxports are shared and consumed by files in `tests/`. +#[allow(unused_imports)] +pub use self::{ + temp_storage_ext::TempStorageExt, test_node_builder_ext::BuilderExt, + test_node_ext::TestNodeExt, tracing_subscriber::set_tracing_subscriber, }; /// Penumbra-specific extensions to the mock consensus builder. @@ -22,113 +12,16 @@ use { /// See [`BuilderExt`]. mod test_node_builder_ext; -// 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 = "info,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") - // Without explicitly disabling the `r1cs` target, the ZK proof implementations - // will spend an enormous amount of CPU and memory building useless tracing output. - .add_directive( - "r1cs=off" - .parse() - .expect("rics=off is a valid filter directive"), - ); - - let subscriber = tracing_subscriber::fmt() - .with_env_filter(filter) - .pretty() - .with_test_writer() - .finish(); - - tracing::subscriber::set_default(subscriber) -} - -/// A [`TestNode`] coupled with Penumbra's [`Consensus`] service. -pub type PenumbraTestNode = TestNode; - -/// Returns a new [`PenumbraTestNode`] backed by the given temporary storage. -pub async fn start_test_node(storage: &TempStorage) -> anyhow::Result { - use tap::TapFallible; - 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 - .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain")) -} - -#[async_trait] -pub trait TempStorageExt: Sized { - async fn apply_genesis(self, genesis: AppState) -> anyhow::Result; - async fn apply_default_genesis(self) -> anyhow::Result; -} +/// Extensions to [`TempStorage`][cnidarium::TempStorage]. +mod temp_storage_ext; -#[async_trait] -impl TempStorageExt for TempStorage { - async fn apply_genesis(self, genesis: AppState) -> anyhow::Result { - // Check that we haven't already applied a genesis state: - if self.latest_version() != u64::MAX { - anyhow::bail!("database already initialized"); - } - - // Apply the genesis state to the storage - let mut app = App::new(self.latest_snapshot()).await?; - app.init_chain(&genesis).await; - app.commit(self.deref().clone()).await; - - Ok(self) - } - - async fn apply_default_genesis(self) -> anyhow::Result { - self.apply_genesis(Default::default()).await - } -} - -#[async_trait] -pub trait TestNodeExt: Sized { - async fn fast_forward_to_next_epoch( - &mut self, - storage: &TempStorage, - ) -> anyhow::Result; -} - -#[async_trait] -impl TestNodeExt for TestNode -where - C: tower::Service< - tendermint::v0_37::abci::ConsensusRequest, - Response = tendermint::v0_37::abci::ConsensusResponse, - Error = tower::BoxError, - > + Send - + Clone - + 'static, - C::Future: Send + 'static, - C::Error: Sized, -{ - async fn fast_forward_to_next_epoch( - &mut self, - storage: &TempStorage, - ) -> Result { - use {penumbra_sct::component::clock::EpochRead, tap::Tap}; - - let get_epoch = || async { storage.latest_snapshot().get_current_epoch().await }; - let start = get_epoch() - .await? - .tap(|start| tracing::info!(?start, "fast forwarding to next epoch")); +/// Penumbra-specific extensions to the mock consensus test node. +/// +/// See [`TestNodeExt`]. +mod test_node_ext; - loop { - self.block().execute().await?; - let current = get_epoch().await?; - if current != start { - tracing::debug!(end = ?current, ?start, "reached next epoch"); - return Ok(current); - } - } - } -} +/// A pretty [`tracing`] subscriber for use in test cases. +/// +/// NB: this subscriber makes use of a test writer, that is compatible with `cargo test`'s output +/// capturing. +mod tracing_subscriber; diff --git a/crates/core/app/tests/common/temp_storage_ext.rs b/crates/core/app/tests/common/temp_storage_ext.rs new file mode 100644 index 0000000000..5ae1524e03 --- /dev/null +++ b/crates/core/app/tests/common/temp_storage_ext.rs @@ -0,0 +1,33 @@ +use { + async_trait::async_trait, + cnidarium::TempStorage, + penumbra_app::{app::App, genesis::AppState}, + std::ops::Deref, +}; + +#[async_trait] +pub trait TempStorageExt: Sized { + async fn apply_genesis(self, genesis: AppState) -> anyhow::Result; + async fn apply_default_genesis(self) -> anyhow::Result; +} + +#[async_trait] +impl TempStorageExt for TempStorage { + async fn apply_genesis(self, genesis: AppState) -> anyhow::Result { + // Check that we haven't already applied a genesis state: + if self.latest_version() != u64::MAX { + anyhow::bail!("database already initialized"); + } + + // Apply the genesis state to the storage + let mut app = App::new(self.latest_snapshot()).await?; + app.init_chain(&genesis).await; + app.commit(self.deref().clone()).await; + + Ok(self) + } + + async fn apply_default_genesis(self) -> anyhow::Result { + self.apply_genesis(Default::default()).await + } +} diff --git a/crates/core/app/tests/common/test_node_builder_ext.rs b/crates/core/app/tests/common/test_node_builder_ext.rs index 0e586dfdcd..b074232cb5 100644 --- a/crates/core/app/tests/common/test_node_builder_ext.rs +++ b/crates/core/app/tests/common/test_node_builder_ext.rs @@ -1,11 +1,16 @@ use { + decaf377_rdsa::VerificationKey, penumbra_app::genesis::AppState, + penumbra_keys::keys::{SpendKey, SpendKeyBytes}, penumbra_mock_consensus::builder::Builder, penumbra_proto::{ core::keys::v1::{GovernanceKey, IdentityKey}, penumbra::core::component::stake::v1::Validator as PenumbraValidator, }, penumbra_shielded_pool::genesis::Allocation, + penumbra_stake::DelegationToken, + rand::Rng, + rand_core::OsRng, tracing::trace, }; @@ -57,12 +62,6 @@ impl BuilderExt for Builder { fn generate_penumbra_validator( consensus_key: &ed25519_consensus::VerificationKey, ) -> (PenumbraValidator, Allocation) { - use decaf377_rdsa::VerificationKey; - use penumbra_keys::keys::{SpendKey, SpendKeyBytes}; - use penumbra_stake::DelegationToken; - use rand::Rng; - use rand_core::OsRng; - let seed = SpendKeyBytes(OsRng.gen()); let spend_key = SpendKey::from(seed.clone()); let validator_id_sk = spend_key.spend_auth_key(); diff --git a/crates/core/app/tests/common/test_node_ext.rs b/crates/core/app/tests/common/test_node_ext.rs new file mode 100644 index 0000000000..48a7f55fd2 --- /dev/null +++ b/crates/core/app/tests/common/test_node_ext.rs @@ -0,0 +1,45 @@ +use { + async_trait::async_trait, cnidarium::TempStorage, penumbra_mock_consensus::TestNode, + penumbra_sct::component::clock::EpochRead as _, tap::Tap, +}; + +#[async_trait] +pub trait TestNodeExt: Sized { + async fn fast_forward_to_next_epoch( + &mut self, + storage: &TempStorage, + ) -> anyhow::Result; +} + +#[async_trait] +impl TestNodeExt for TestNode +where + C: tower::Service< + tendermint::v0_37::abci::ConsensusRequest, + Response = tendermint::v0_37::abci::ConsensusResponse, + Error = tower::BoxError, + > + Send + + Clone + + 'static, + C::Future: Send + 'static, + C::Error: Sized, +{ + async fn fast_forward_to_next_epoch( + &mut self, + storage: &TempStorage, + ) -> Result { + let get_epoch = || async { storage.latest_snapshot().get_current_epoch().await }; + let start = get_epoch() + .await? + .tap(|start| tracing::info!(?start, "fast forwarding to next epoch")); + + loop { + self.block().execute().await?; + let current = get_epoch().await?; + if current != start { + tracing::debug!(end = ?current, ?start, "reached next epoch"); + return Ok(current); + } + } + } +} diff --git a/crates/core/app/tests/common/tracing_subscriber.rs b/crates/core/app/tests/common/tracing_subscriber.rs new file mode 100644 index 0000000000..52597956d7 --- /dev/null +++ b/crates/core/app/tests/common/tracing_subscriber.rs @@ -0,0 +1,29 @@ +use { + tracing::subscriber::{set_default, DefaultGuard}, + tracing_subscriber::{filter::EnvFilter, fmt}, +}; + +/// Installs a tracing subscriber to log events until the returned guard is dropped. +// NB: this is marked as "dead code" but it is used by integration tests. +#[allow(dead_code)] +pub fn set_tracing_subscriber() -> DefaultGuard { + let filter = "info,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") + // Without explicitly disabling the `r1cs` target, the ZK proof implementations + // will spend an enormous amount of CPU and memory building useless tracing output. + .add_directive( + "r1cs=off" + .parse() + .expect("rics=off is a valid filter directive"), + ); + + let subscriber = fmt() + .with_env_filter(filter) + .pretty() + .with_test_writer() + .finish(); + + set_default(subscriber) +} diff --git a/crates/core/app/tests/init_chain.rs b/crates/core/app/tests/init_chain.rs deleted file mode 100644 index b983f3c420..0000000000 --- a/crates/core/app/tests/init_chain.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! App integration tests using mock consensus. -// -// Note: these should eventually replace the existing test cases. mock consensus tests are placed -// here while the engine is still in development. See #3588. - -mod common; - -use { - anyhow::anyhow, cnidarium::TempStorage, penumbra_sct::component::clock::EpochRead, - penumbra_stake::component::validator_handler::ValidatorDataRead as _, tap::Tap, tracing::info, -}; - -/// Exercises that a test node can be instantiated using the consensus service. -#[tokio::test] -async fn mock_consensus_can_send_an_init_chain_request() -> anyhow::Result<()> { - // Install a test logger, acquire some temporary storage, and start the test node. - let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; - let _ = common::start_test_node(&storage).await?; - - // Free our temporary storage. - drop(storage); - drop(guard); - - Ok(()) -} - -/// Exercises that the mock consensus engine can provide a single genesis validator. -#[tokio::test] -async fn mock_consensus_can_define_a_genesis_validator() -> anyhow::Result<()> { - // Install a test logger, acquire some temporary storage, and start the test node. - let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; - let _test_node = common::start_test_node(&storage).await?; - - let snapshot = storage.latest_snapshot(); - let validators = snapshot - .validator_definitions() - .tap(|_| info!("getting validator definitions")) - .await?; - match validators.as_slice() { - [v] => { - let identity_key = v.identity_key; - let status = snapshot - .get_validator_state(&identity_key) - .await? - .ok_or_else(|| anyhow!("could not find validator status"))?; - assert_eq!( - status, - penumbra_stake::validator::State::Active, - "validator should be active" - ); - } - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - } - - // Free our temporary storage. - drop(storage); - drop(guard); - - Ok(()) -} - -/// Exercises that a series of empty blocks, with no validator set present, can be successfully -/// executed by the consensus service. -#[tokio::test] -async fn mock_consensus_can_send_a_sequence_of_empty_blocks() -> anyhow::Result<()> { - // Install a test logger, acquire some temporary storage, and start the test node. - let guard = common::set_tracing_subscriber(); - let storage = TempStorage::new().await?; - let mut test_node = common::start_test_node(&storage).await?; - - let height = || async { storage.latest_snapshot().get_block_height().await }; - - // Fast forward eight blocks, and show that the height is 8 after doing so. - assert_eq!(height().await?, 0, "height should begin at 0"); - test_node.fast_forward(8).await?; - assert_eq!(height().await?, 8_u64, "height should grow"); - - // Free our temporary storage. - drop(storage); - drop(guard); - - Ok(()) -} diff --git a/crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs b/crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs new file mode 100644 index 0000000000..e1954bc743 --- /dev/null +++ b/crates/core/app/tests/mock_consensus_can_define_a_genesis_validator.rs @@ -0,0 +1,58 @@ +use { + self::common::BuilderExt, + anyhow::anyhow, + cnidarium::TempStorage, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_mock_consensus::TestNode, + penumbra_stake::component::validator_handler::ValidatorDataRead as _, + tap::{Tap, TapFallible}, + tracing::info, +}; + +mod common; + +/// Exercises that the mock consensus engine can provide a single genesis validator. +#[tokio::test] +async fn mock_consensus_can_define_a_genesis_validator() -> anyhow::Result<()> { + // Install a test logger, acquire some temporary storage, and start the test node. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + let test_node = { + 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 + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + let snapshot = storage.latest_snapshot(); + let validators = snapshot + .validator_definitions() + .tap(|_| info!("getting validator definitions")) + .await?; + match validators.as_slice() { + [v] => { + let identity_key = v.identity_key; + let status = snapshot + .get_validator_state(&identity_key) + .await? + .ok_or_else(|| anyhow!("could not find validator status"))?; + assert_eq!( + status, + penumbra_stake::validator::State::Active, + "validator should be active" + ); + } + unexpected => panic!("there should be one validator, got: {unexpected:?}"), + } + + // Free our temporary storage. + drop(test_node); + drop(storage); + drop(guard); + + Ok(()) +} diff --git a/crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs b/crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs new file mode 100644 index 0000000000..74e662285b --- /dev/null +++ b/crates/core/app/tests/mock_consensus_can_send_a_sequence_of_empty_blocks.rs @@ -0,0 +1,43 @@ +use { + self::common::BuilderExt, + cnidarium::TempStorage, + penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_mock_consensus::TestNode, + penumbra_sct::component::clock::EpochRead as _, + tap::TapFallible, +}; + +mod common; + +/// Exercises that a series of empty blocks, with no validator set present, can be successfully +/// executed by the consensus service. +#[tokio::test] +async fn mock_consensus_can_send_a_sequence_of_empty_blocks() -> anyhow::Result<()> { + // Install a test logger, acquire some temporary storage, and start the test node. + let guard = common::set_tracing_subscriber(); + let storage = TempStorage::new().await?; + let mut test_node = { + 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 + .tap_ok(|e| tracing::info!(hash = %e.last_app_hash_hex(), "finished init chain"))? + }; + + let height = || async { storage.latest_snapshot().get_block_height().await }; + + // Fast forward eight blocks, and show that the height is 8 after doing so. + assert_eq!(height().await?, 0, "height should begin at 0"); + test_node.fast_forward(8).await?; + assert_eq!(height().await?, 8_u64, "height should grow"); + + // Free our temporary storage. + drop(test_node); + drop(storage); + drop(guard); + + Ok(()) +}