diff --git a/Cargo.lock b/Cargo.lock index cc561874ac..0cf86496b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5273,6 +5273,12 @@ version = "0.68.0" dependencies = [ "anyhow", "bytes", + "decaf377-rdsa", + "ed25519-consensus", + "penumbra-keys", + "penumbra-proto", + "rand 0.8.5", + "rand_core 0.6.4", "serde_json", "tap", "tendermint", diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index f400c349fb..91798d7d16 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -3,15 +3,19 @@ // 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, - server::consensus::{Consensus, ConsensusService}, +use { + async_trait::async_trait, + cnidarium::TempStorage, + penumbra_app::{ + app::App, + server::consensus::{Consensus, ConsensusService}, + }, + penumbra_genesis::AppState, + penumbra_mock_consensus::TestNode, + std::ops::Deref, + tap::Tap, + tracing::{trace, warn}, }; -use penumbra_genesis::AppState; -use penumbra_mock_consensus::TestNode; -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 { @@ -83,19 +87,63 @@ impl TempStorageExt for TempStorage { /// Penumbra-specific extensions to the mock consensus builder. pub trait BuilderExt: Sized { + /// The error thrown by [`with_penumbra_auto_app_state`] type Error; + /// Add the provided Penumbra [`AppState`] to the builder. + /// + /// This will inject any configured validators into the state before serializing it into bytes. fn with_penumbra_auto_app_state(self, app_state: AppState) -> Result; } impl BuilderExt for penumbra_mock_consensus::builder::Builder { type Error = anyhow::Error; fn with_penumbra_auto_app_state(self, app_state: AppState) -> Result { - // what to do here? - // - read out list of abci/comet validators from the builder, - // - define a penumbra validator for each one - // - inject that into the penumbra app state - // - serialize to json and then call `with_app_state_bytes` - let app_state = serde_json::to_vec(&app_state)?; - Ok(self.app_state(app_state)) + use penumbra_proto::penumbra::core::component::stake::v1 as pb; + + // Take the list of genesis validators from the builder... + let validators = self + .validators + .iter() + .inspect( + |pb::Validator { + name, + enabled, + sequence_number, + .. + }| { + // ...log the name of each... + trace!(%name, %enabled, %sequence_number, "injecting validator into app state") + }, + ) + .cloned() + .collect::>() + .tap(|v| { + // ...or print a warning if there are not any validators. + if v.is_empty() { + warn!("`with_penumbra_auto_app_state` was called but builder has no validators") + } + }); + + // Add the validators to the app state. + let app_state: AppState = match app_state { + AppState::Checkpoint(_) => anyhow::bail!("checkpoint app state isn't supported"), + AppState::Content(mut content) => { + // Inject the builder's validators into the staking component's genesis state. + std::mem::replace( + &mut content.stake_content.validators, + validators + ) + .tap(|overwritten| { + // Log a warning if this overwrote any validators already in the app state. + warn!(?overwritten, "`with_penumbra_auto_app_state` overwrote validators in the given AppState") + }); + AppState::Content(content) + } + }; + + // Serialize the app state into bytes, and add it to the builder. + serde_json::to_vec(&app_state) + .map_err(Self::Error::from) + .map(|s| self.app_state(s)) } } diff --git a/crates/core/app/tests/mock_consensus.rs b/crates/core/app/tests/mock_consensus.rs index c68b8e3397..7b99ee63b0 100644 --- a/crates/core/app/tests/mock_consensus.rs +++ b/crates/core/app/tests/mock_consensus.rs @@ -3,6 +3,9 @@ // 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 penumbra_ibc::component::ClientStateReadExt; +use penumbra_stake::component::validator_handler::ValidatorDataRead; + mod common; use { @@ -37,6 +40,24 @@ async fn mock_consensus_can_send_an_init_chain_request() -> anyhow::Result<()> { 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 identity_key = test_node.identity_key; + storage.latest_snapshot().get_validator_status(identity_key); + + // 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] diff --git a/crates/test/mock-consensus/Cargo.toml b/crates/test/mock-consensus/Cargo.toml index 5bbdd37521..d471fdefeb 100644 --- a/crates/test/mock-consensus/Cargo.toml +++ b/crates/test/mock-consensus/Cargo.toml @@ -10,6 +10,12 @@ license.workspace = true [dependencies] anyhow = { workspace = true } bytes = { workspace = true } +decaf377-rdsa = { workspace = true } +ed25519-consensus = { workspace = true } +penumbra-proto = { workspace = true } +penumbra-keys = { workspace = true } +rand = {workspace = true} +rand_core = {workspace = true} serde_json = { workspace = true } tap = { workspace = true } tendermint = { workspace = true } diff --git a/crates/test/mock-consensus/src/builder.rs b/crates/test/mock-consensus/src/builder.rs index 7d90e11c1f..1f74545bf2 100644 --- a/crates/test/mock-consensus/src/builder.rs +++ b/crates/test/mock-consensus/src/builder.rs @@ -5,12 +5,16 @@ /// Most importantly, defines [`Builder::init_chain()`]. mod init_chain; -use {crate::TestNode, bytes::Bytes}; +use { + crate::TestNode, bytes::Bytes, penumbra_proto::penumbra::core::component::stake::v1 as pb, + rand_core::OsRng, +}; /// A buider, used to prepare and instantiate a new [`TestNode`]. #[derive(Default)] pub struct Builder { - app_state: Option, + pub app_state: Option, + pub validators: Vec, } impl TestNode<()> { @@ -21,12 +25,54 @@ impl TestNode<()> { } 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 + use penumbra_proto::core::keys::v1::{GovernanceKey, IdentityKey}; + + // Generate a spend authoration key. + let bytes = { + use { + decaf377_rdsa::VerificationKey, + penumbra_keys::keys::{SpendKey, SpendKeyBytes}, + rand::Rng, + }; + let spend_key = SpendKey::from(SpendKeyBytes(OsRng.gen())); + let spend_auth_key = spend_key.spend_auth_key(); + let verification_key = VerificationKey::from(spend_auth_key); + verification_key.to_bytes().to_vec() + }; + + // Generate an identity key for the validator. + let identity_key = Some(IdentityKey { ik: bytes.clone() }); + + // Generate a governance key for the validator. + // + // NB: for now, we will use the same key for governance. See the documentation of + // `GovernanceKey` for more information about cold storage of validator keys. + let governance_key = Some(GovernanceKey { gk: bytes.clone() }); + + // Generate a consensus pubkey for the validator. + let consensus_key = { + let signing_key = ed25519_consensus::SigningKey::new(OsRng); + signing_key.as_bytes().as_slice().to_vec() + }; + + let validator = pb::Validator { + identity_key, + consensus_key, + enabled: true, + sequence_number: 0, + governance_key, + name: String::default(), + website: String::default(), + description: String::default(), + funding_streams: Vec::default(), + }; + + Self { + validators: vec![validator], + ..self + } } /// Sets the `app_state_bytes` to send the ABCI application upon chain initialization. diff --git a/crates/test/mock-consensus/src/builder/init_chain.rs b/crates/test/mock-consensus/src/builder/init_chain.rs index b60a7793fd..93bbc23e97 100644 --- a/crates/test/mock-consensus/src/builder/init_chain.rs +++ b/crates/test/mock-consensus/src/builder/init_chain.rs @@ -32,6 +32,7 @@ impl Builder { let Self { app_state: Some(app_state), + validators: _, } = self else { bail!("builder was not fully initialized")