diff --git a/crates/core/app/src/genesis.rs b/crates/core/app/src/genesis.rs index 85380713cb..942cd80331 100644 --- a/crates/core/app/src/genesis.rs +++ b/crates/core/app/src/genesis.rs @@ -164,6 +164,30 @@ impl DomainType for AppState { type Proto = pb::GenesisAppState; } +impl Content { + pub fn with_epoch_duration(self, epoch_duration: u64) -> Self { + Self { + sct_content: penumbra_sct::genesis::Content { + sct_params: penumbra_sct::params::SctParameters { epoch_duration }, + }, + ..self + } + } + + pub fn with_unbonding_delay(self, unbonding_delay: u64) -> Self { + Self { + stake_content: penumbra_stake::genesis::Content { + stake_params: penumbra_stake::params::StakeParameters { + unbonding_delay, + ..self.stake_content.stake_params + }, + ..self.stake_content + }, + ..self + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs index 4d59dfab20..a3b26cfa52 100644 --- a/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs +++ b/crates/core/app/tests/app_can_define_and_delegate_to_a_validator.rs @@ -1,11 +1,12 @@ -mod common; - use { - self::common::BuilderExt, - anyhow::{anyhow, Context}, + self::common::{BuilderExt, TestNodeExt}, + anyhow::anyhow, cnidarium::TempStorage, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, - penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_app::{ + genesis::{self, AppState}, + server::consensus::Consensus, + }, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, @@ -19,6 +20,8 @@ use { tracing::{error_span, info, Instrument}, }; +mod common; + /// The length of the [`penumbra_sct`] epoch. /// /// This test relies on many epochs turning over, so we will work with a shorter epoch duration. @@ -31,14 +34,8 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { let storage = TempStorage::new().await?; // Configure an AppState with slightly shorter epochs than usual. - let app_state = AppState::Content(penumbra_app::genesis::Content { - sct_content: penumbra_sct::genesis::Content { - sct_params: penumbra_sct::params::SctParameters { - epoch_duration: EPOCH_DURATION, - }, - }, - ..Default::default() - }); + let app_state = + AppState::Content(genesis::Content::default().with_epoch_duration(EPOCH_DURATION)); // Start the test node. let mut node = { @@ -58,24 +55,16 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { // Fast forward to the next epoch. let snapshot_start = storage.latest_snapshot(); - node.fast_forward(EPOCH_DURATION) - .instrument(error_span!("fast forwarding test node to second epoch")) - .await - .context("fast forwarding {EPOCH_LENGTH} blocks")?; + node.fast_forward_to_next_epoch(&storage).await?; let snapshot_end = storage.latest_snapshot(); // Retrieve the validator definition from the latest snapshot. - let existing_validator = match snapshot_end - .validator_definitions() - .tap(|_| info!("getting validator definitions")) + let [existing_validator_id] = storage + .latest_snapshot() + .validator_identity_keys() .await? - .as_slice() - { - [v] => v.clone(), - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - }; - - let existing_validator_id = existing_validator.identity_key; + .try_into() + .map_err(|keys| anyhow::anyhow!("expected one key, got: {keys:?}"))?; // Check that we are now in a new epoch. { @@ -340,12 +329,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { }; // Fast forward to the next epoch. - node.fast_forward(EPOCH_DURATION) - .instrument(error_span!( - "fast forwarding test node to epoch after delegation" - )) - .await - .context("fast forwarding {EPOCH_LENGTH} blocks")?; + node.fast_forward_to_next_epoch(&storage).await?; let post_delegate_next_epoch_snapshot = storage.latest_snapshot(); // Show that now, after an epoch and with a delegation, the validator is marked active. @@ -491,12 +475,7 @@ async fn app_can_define_and_delegate_to_a_validator() -> anyhow::Result<()> { }); // Fast forward to the next epoch. - node.fast_forward(EPOCH_DURATION) - .instrument(error_span!( - "fast forwarding test node to epoch after undelegation" - )) - .await - .context("fast forwarding {EPOCH_LENGTH} blocks")?; + node.fast_forward_to_next_epoch(&storage).await?; let post_undelegate_next_epoch_snapshot = storage.latest_snapshot(); // Show that after undelegating, the validator is no longer marked active. diff --git a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs index de61fd1132..1f7f2c7573 100644 --- a/crates/core/app/tests/app_can_undelegate_from_a_validator.rs +++ b/crates/core/app/tests/app_can_undelegate_from_a_validator.rs @@ -1,22 +1,19 @@ -use penumbra_num::fixpoint::U128x128; - -mod common; - use { - self::common::BuilderExt, + self::common::{BuilderExt, TestNodeExt}, anyhow::anyhow, ark_ff::UniformRand, cnidarium::TempStorage, - penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_app::{ + genesis::{self, AppState}, + server::consensus::Consensus, + }, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, + penumbra_num::fixpoint::U128x128, penumbra_proto::DomainType, penumbra_sct::component::clock::EpochRead as _, - penumbra_stake::{ - component::validator_handler::ValidatorDataRead as _, validator::Validator, - UndelegateClaimPlan, - }, + penumbra_stake::{component::validator_handler::ValidatorDataRead as _, UndelegateClaimPlan}, penumbra_transaction::{ memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan, }, @@ -25,6 +22,8 @@ use { tracing::{error_span, info, Instrument}, }; +mod common; + /// The length of the [`penumbra_sct`] epoch. /// /// This test relies on many epochs turning over, so we will work with a shorter epoch duration. @@ -58,21 +57,11 @@ async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { }; // Configure an AppState with slightly shorter epochs than usual. - let app_state = AppState::Content(penumbra_app::genesis::Content { - sct_content: penumbra_sct::genesis::Content { - sct_params: penumbra_sct::params::SctParameters { - epoch_duration: EPOCH_DURATION, - }, - }, - stake_content: penumbra_stake::genesis::Content { - stake_params: penumbra_stake::params::StakeParameters { - unbonding_delay: UNBONDING_DELAY, - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - }); + let app_state = AppState::Content( + genesis::Content::default() + .with_epoch_duration(EPOCH_DURATION) + .with_unbonding_delay(UNBONDING_DELAY), + ); // Start the test node. let mut node = { @@ -85,16 +74,12 @@ async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { }?; // Retrieve the validator definition from the latest snapshot. - let Validator { identity_key, .. } = match storage + let [identity_key] = storage .latest_snapshot() - .validator_definitions() - .tap(|_| info!("getting validator definitions")) + .validator_identity_keys() .await? - .as_slice() - { - [v] => v.clone(), - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - }; // ..and note the asset id for delegation tokens tied to this validator. + .try_into() + .map_err(|keys| anyhow::anyhow!("expected one key, got: {keys:?}"))?; let delegate_token_id = penumbra_stake::DelegationToken::new(identity_key).id(); // Sync the mock client, using the test wallet's spend key, to the latest snapshot. @@ -203,12 +188,7 @@ async fn app_can_undelegate_from_a_validator() -> anyhow::Result<()> { } // Fast forward to the next epoch. - { - let start = get_latest_epoch().await.index; - while get_latest_epoch().await.index < start { - node.block().execute().await?; - } - } + node.fast_forward_to_next_epoch(&storage).await?; // Build a transaction that will now undelegate from the validator. let undelegate_rate = storage diff --git a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs index e5a76d2e41..126888c365 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_missing_blocks.rs @@ -6,11 +6,9 @@ use { cnidarium::TempStorage, penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_mock_consensus::TestNode, - penumbra_stake::{ - component::validator_handler::validator_store::ValidatorDataRead, validator::Validator, - }, + penumbra_stake::component::validator_handler::validator_store::ValidatorDataRead, tap::Tap, - tracing::{error_span, info, trace, Instrument}, + tracing::{error_span, trace, Instrument}, }; #[tokio::test] @@ -31,16 +29,12 @@ async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Res }?; // Retrieve the validator definition from the latest snapshot. - let Validator { identity_key, .. } = match storage + let [identity_key] = storage .latest_snapshot() - .validator_definitions() - .tap(|_| info!("getting validator definitions")) + .validator_identity_keys() .await? - .as_slice() - { - [v] => v.clone(), - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - }; + .try_into() + .map_err(|keys| anyhow::anyhow!("expected one key, got: {keys:?}"))?; let get_uptime = || async { storage .latest_snapshot() diff --git a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs index 7d0e3ba9c4..8c9be9a2d2 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_genesis_validator_signing_blocks.rs @@ -1,18 +1,16 @@ -mod common; - use { self::common::BuilderExt, anyhow::Context, cnidarium::TempStorage, penumbra_app::{genesis::AppState, server::consensus::Consensus}, penumbra_mock_consensus::TestNode, - penumbra_stake::{ - component::validator_handler::validator_store::ValidatorDataRead, validator::Validator, - }, + penumbra_stake::component::validator_handler::validator_store::ValidatorDataRead, tap::Tap, - tracing::{error_span, info, Instrument}, + tracing::{error_span, Instrument}, }; +mod common; + #[tokio::test] async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Result<()> { // Install a test logger, acquire some temporary storage, and start the test node. @@ -31,16 +29,12 @@ async fn app_tracks_uptime_for_genesis_validator_missing_blocks() -> anyhow::Res }?; // Retrieve the validator definition from the latest snapshot. - let Validator { identity_key, .. } = match storage + let [identity_key] = storage .latest_snapshot() - .validator_definitions() - .tap(|_| info!("getting validator definitions")) + .validator_identity_keys() .await? - .as_slice() - { - [v] => v.clone(), - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - }; + .try_into() + .map_err(|keys| anyhow::anyhow!("expected one key, got: {keys:?}"))?; let get_uptime = || async { storage .latest_snapshot() diff --git a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs index de687e553c..8a4553e786 100644 --- a/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs +++ b/crates/core/app/tests/app_tracks_uptime_for_validators_only_once_active.rs @@ -1,10 +1,11 @@ -mod common; - use { - self::common::BuilderExt, + self::common::{BuilderExt, TestNodeExt}, cnidarium::TempStorage, decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey}, - penumbra_app::{genesis::AppState, server::consensus::Consensus}, + penumbra_app::{ + genesis::{self, AppState}, + server::consensus::Consensus, + }, penumbra_keys::test_keys, penumbra_mock_client::MockClient, penumbra_mock_consensus::TestNode, @@ -16,9 +17,11 @@ use { }, rand_core::OsRng, tap::Tap, - tracing::{error_span, info, Instrument}, + tracing::{error_span, Instrument}, }; +mod common; + #[tokio::test] async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<()> { /// The length of the [`penumbra_sct`] epoch. @@ -31,14 +34,8 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( let storage = TempStorage::new().await?; // Configure an AppState with slightly shorter epochs than usual. - let app_state = AppState::Content(penumbra_app::genesis::Content { - sct_content: penumbra_sct::genesis::Content { - sct_params: penumbra_sct::params::SctParameters { - epoch_duration: EPOCH_DURATION, - }, - }, - ..Default::default() - }); + let app_state = + AppState::Content(genesis::Content::default().with_epoch_duration(EPOCH_DURATION)); // Start the test node. let mut node = { @@ -73,19 +70,12 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( }; // Get the identity key of the genesis validator, before we go further. - // Retrieve the validator definition from the latest snapshot. - let existing_validator_id = { - use penumbra_stake::component::validator_handler::validator_store::ValidatorDataRead; - let validators = &storage - .latest_snapshot() - .validator_definitions() - .tap(|_| info!("getting validator definitions")) - .await?; - match validators.as_slice() { - [Validator { identity_key, .. }] => *identity_key, - unexpected => panic!("there should be one validator, got: {unexpected:?}"), - } - }; + let [existing_validator_id] = storage + .latest_snapshot() + .validator_identity_keys() + .await? + .try_into() + .map_err(|keys| anyhow::anyhow!("expected one key, got: {keys:?}"))?; // To define a validator, we need to define two keypairs: an identity key // for the Penumbra application and a consensus key for cometbft. @@ -237,19 +227,10 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( } // Fast forward to the next epoch. + node.fast_forward_to_next_epoch(&storage).await?; + + // The new validator should now be in the consensus set. { - 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")); - let next = loop { - node.block().execute().await?; - let current = get_epoch().await?; - if current != start { - break current; - } - }; - tracing::info!(?start, ?next, "finished fast forwarding to next epoch"); assert_eq!( get_latest_consensus_set().await.len(), 2, @@ -366,19 +347,10 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( ); // Fast forward to the next epoch. + node.fast_forward_to_next_epoch(&storage).await?; + + // The validator should no longer be part of the consensus set. { - 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")); - let next = loop { - node.block().execute().await?; - let current = get_epoch().await?; - if current != start { - break current; - } - }; - tracing::info!(?start, ?next, "finished fast forwarding to next epoch"); assert_eq!( get_latest_consensus_set().await.len(), 1, @@ -406,19 +378,10 @@ async fn app_tracks_uptime_for_validators_only_once_active() -> anyhow::Result<( } // Fast forward to the next epoch. + node.fast_forward_to_next_epoch(&storage).await?; + + // There should only be one validator in the consensus set. { - 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")); - let next = loop { - node.block().execute().await?; - let current = get_epoch().await?; - if current != start { - break current; - } - }; - tracing::info!(?start, ?next, "finished fast forwarding to next epoch"); assert_eq!( get_latest_consensus_set().await.len(), 1, diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index 38f9af1d74..043d3d39f0 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -89,3 +89,46 @@ impl TempStorageExt for TempStorage { 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")); + + 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/component/stake/src/component/validator_handler/validator_store.rs b/crates/core/component/stake/src/component/validator_handler/validator_store.rs index 89abf01686..3f30895d12 100644 --- a/crates/core/component/stake/src/component/validator_handler/validator_store.rs +++ b/crates/core/component/stake/src/component/validator_handler/validator_store.rs @@ -235,6 +235,15 @@ pub trait ValidatorDataRead: StateRead { .try_collect() .await } + + /// Returns a list of **all** known validators identity keys. + async fn validator_identity_keys(&self) -> Result> { + self.prefix(state_key::validators::definitions::prefix()) + .map_ok(|(_key, validator)| validator) + .map_ok(|validator: Validator| validator.identity_key) + .try_collect() + .await + } } impl ValidatorDataRead for T {}