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(()) +}