diff --git a/Cargo.lock b/Cargo.lock index f7fc7f009b..bfba8debe4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5272,7 +5272,9 @@ dependencies = [ "anyhow", "bytes", "decaf377-rdsa", + "ed25519-consensus", "http", + "rand_core 0.6.4", "tap", "tendermint", "tower", diff --git a/crates/core/app/tests/common/mod.rs b/crates/core/app/tests/common/mod.rs index d7904e38c6..f47e16d436 100644 --- a/crates/core/app/tests/common/mod.rs +++ b/crates/core/app/tests/common/mod.rs @@ -3,6 +3,8 @@ // NB: Allow dead code, these are in fact shared by files in `tests/`. #![allow(dead_code)] +pub use self::test_node_builder_ext::BuilderExt; + use { async_trait::async_trait, cnidarium::TempStorage, @@ -13,10 +15,13 @@ use { penumbra_genesis::AppState, penumbra_mock_consensus::TestNode, std::ops::Deref, - tap::Tap, - tracing::{trace, warn}, }; +/// Penumbra-specific extensions to the mock consensus builder. +/// +/// 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; @@ -51,7 +56,7 @@ pub async fn start_test_node(storage: &TempStorage) -> anyhow::Result Result; - /// Creates a single validator with a randomly generated key. - /// - /// This will set the builder's identity key. - fn with_penumbra_single_validator(self) -> Self; -} - -impl BuilderExt for penumbra_mock_consensus::builder::Builder { - type Error = anyhow::Error; - fn with_penumbra_auto_app_state(self, app_state: AppState) -> Result { - use penumbra_proto::penumbra::core::component::stake::v1 as pb; - - // Take the list of genesis validators from the builder... - let validators = self - .extensions - .get::>() - .ok_or_else(|| { - anyhow::anyhow!("`with_penumbra_auto_app_state` could not find validators") - })? - .clone() - .tap(|v| { - for pb::Validator { - name, - enabled, - sequence_number, - .. - } in v - { - // ...log the name of each... - trace!(%name, %enabled, %sequence_number, "injecting validator into app state"); - } - // ...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. - if !overwritten.is_empty() { - 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)) - } - - fn with_penumbra_single_validator(self) -> Self { - use { - decaf377_rdsa::VerificationKey, - penumbra_keys::keys::{SpendKey, SpendKeyBytes}, - penumbra_proto::{ - core::keys::v1::{GovernanceKey, IdentityKey}, - penumbra::core::component::stake::v1::Validator, - }, - rand::Rng, - rand_core::OsRng, - }; - - // Generate a spend authoration key. - let bytes = { - 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() - }; - - // Generate a validator entry using the generated key. - let validator = Validator { - identity_key: Some(IdentityKey { - ik: bytes.to_vec().clone(), - }), - governance_key: Some(GovernanceKey { - // 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. - gk: bytes.to_vec().clone(), - }), - consensus_key: { - let signing_key = ed25519_consensus::SigningKey::new(OsRng); - signing_key.as_bytes().as_slice().to_vec() - }, - enabled: true, - sequence_number: 0, - name: String::default(), - website: String::default(), - description: String::default(), - funding_streams: Vec::default(), - }; - let validators = vec![validator]; - - // Add the generated identity key and the validator information to the builder. - self.identity_key(bytes).extension(validators) - } -} - -pub trait TestNodeExt { - fn penumbra_identity_key(&self) -> penumbra_stake::IdentityKey; -} - -impl TestNodeExt for TestNode { - fn penumbra_identity_key(&self) -> penumbra_stake::IdentityKey { - self.identity_key() - .try_into() - .map(penumbra_stake::IdentityKey) - .expect("test node should have a valid identity key") - } -} diff --git a/crates/core/app/tests/common/test_node_builder_ext.rs b/crates/core/app/tests/common/test_node_builder_ext.rs new file mode 100644 index 0000000000..0644e771ec --- /dev/null +++ b/crates/core/app/tests/common/test_node_builder_ext.rs @@ -0,0 +1,120 @@ +use { + penumbra_genesis::AppState, + penumbra_mock_consensus::{builder::Builder, keyring::Keys}, + penumbra_proto::{ + core::keys::v1::{GovernanceKey, IdentityKey}, + penumbra::core::component::stake::v1::Validator as PenumbraValidator, + }, + tap::Tap, +}; + +/// 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 Builder { + type Error = anyhow::Error; + fn with_penumbra_auto_app_state(self, app_state: AppState) -> Result { + // Generate a penumbra validator using the test node's consensus keys (if they exist). + // Eventually, we may wish to generate and inject additional definitions, but only a single + // validator is supported for now. + let app_state = match self + .keys + .as_ref() + .map(generate_penumbra_validator) + .inspect(log_validator) + .map(std::iter::once) + { + Some(validator) => app_state_with_validators(app_state, validator)?, + None => app_state, + }; + + // 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)) + } +} + +/// Injects the given collection of [`Validator`s][PenumbraValidator] into the app state. +fn app_state_with_validators( + app_state: AppState, + validators: V, +) -> Result +where + V: IntoIterator, +{ + use AppState::{Checkpoint, Content}; + match app_state { + Checkpoint(_) => anyhow::bail!("checkpoint app state isn't supported"), + Content(mut content) => { + // Inject the builder's validators into the staking component's genesis state. + std::mem::replace( + &mut content.stake_content.validators, + validators.into_iter().collect(), + ) + .tap(|overwritten| { + // Log a warning if this overwrote any validators already in the app state. + if !overwritten.is_empty() { + tracing::warn!( + ?overwritten, + "`with_penumbra_auto_app_state` overwrote validators in the given AppState" + ) + } + }); + Ok(Content(content)) + } + } +} + +/// Generates a [`Validator`][PenumbraValidator] given a set of consensus [`Keys`]. +fn generate_penumbra_validator( + Keys { + consensus_verification_key, + .. + }: &Keys, +) -> PenumbraValidator { + /// A temporary stub for validator keys. + /// + /// 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. + const BYTES: [u8; 32] = [0; 32]; + + PenumbraValidator { + identity_key: Some(IdentityKey { + ik: BYTES.to_vec().clone(), + }), + governance_key: Some(GovernanceKey { + gk: BYTES.to_vec().clone(), + }), + consensus_key: consensus_verification_key.as_bytes().to_vec(), + enabled: true, + sequence_number: 0, + name: String::default(), + website: String::default(), + description: String::default(), + funding_streams: Vec::default(), + } +} + +fn log_validator( + PenumbraValidator { + name, + enabled, + sequence_number, + .. + }: &PenumbraValidator, +) { + tracing::trace!( + %name, + %enabled, + %sequence_number, + "injecting validator into app state" + ) +} diff --git a/crates/core/app/tests/mock_consensus.rs b/crates/core/app/tests/mock_consensus.rs index ccb37a6fd9..5a2c3490b2 100644 --- a/crates/core/app/tests/mock_consensus.rs +++ b/crates/core/app/tests/mock_consensus.rs @@ -6,7 +6,6 @@ mod common; use { - self::common::TestNodeExt as _, anyhow::anyhow, cnidarium::TempStorage, penumbra_keys::test_keys, @@ -45,18 +44,27 @@ 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 _test_node = common::start_test_node(&storage).await?; - let identity_key = test_node.penumbra_identity_key(); - match storage - .latest_snapshot() - .get_validator_state(&identity_key) - .tap(|_| info!(?identity_key, "getting validator state")) - .await? - .ok_or_else(|| anyhow!("genesis validator state was not found"))? - { - penumbra_stake::validator::State::Active => info!("genesis validator is active"), - other => panic!("unexpected genesis validator state, found: {other}"), + 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. diff --git a/crates/test/mock-consensus/Cargo.toml b/crates/test/mock-consensus/Cargo.toml index 773d211b10..ba3e27ce40 100644 --- a/crates/test/mock-consensus/Cargo.toml +++ b/crates/test/mock-consensus/Cargo.toml @@ -15,7 +15,9 @@ license.workspace = true anyhow = { workspace = true } bytes = { workspace = true } decaf377-rdsa = { workspace = true } +ed25519-consensus = { workspace = true } http = { workspace = true } +rand_core = { workspace = true } tap = { workspace = true } tendermint = { workspace = true } tower = { workspace = true, features = ["full"] } diff --git a/crates/test/mock-consensus/src/builder.rs b/crates/test/mock-consensus/src/builder.rs index 53fe4e31c8..29a22cedbb 100644 --- a/crates/test/mock-consensus/src/builder.rs +++ b/crates/test/mock-consensus/src/builder.rs @@ -6,20 +6,15 @@ mod init_chain; use { - crate::TestNode, + crate::{keyring::Keys, TestNode}, bytes::Bytes, - decaf377_rdsa::{SpendAuth, VerificationKeyBytes}, - http::Extensions, - tap::TapOptional, - tracing::warn, }; /// A buider, used to prepare and instantiate a new [`TestNode`]. #[derive(Default)] pub struct Builder { pub app_state: Option, - pub identity_key: Option>, - pub extensions: Extensions, + pub keys: Option, } impl TestNode<()> { @@ -36,26 +31,11 @@ impl Builder { Self { app_state, ..self } } - /// Sets the test node's identity key. - pub fn identity_key(self, identity_key: impl Into>) -> Self { - let identity_key = Some(identity_key.into()); + /// Generates a single set of validator keys. + pub fn single_validator(self) -> Self { Self { - identity_key, + keys: Some(Keys::generate()), ..self } } - - /// Adds an extension to this builder. - /// - /// This is not a part of "regular" use of this builder, but may be used to store additional - /// state to facilitate the implementation of extension traits around this builder. - pub fn extension(mut self, value: T) -> Self - where - T: Send + Sync + 'static, - { - self.extensions - .insert(value) - .tap_some(|_| warn!("builder overwrote an extension value, this is probably a bug!")); - self - } } diff --git a/crates/test/mock-consensus/src/builder/init_chain.rs b/crates/test/mock-consensus/src/builder/init_chain.rs index f7ec0bddec..5d5e02ee3d 100644 --- a/crates/test/mock-consensus/src/builder/init_chain.rs +++ b/crates/test/mock-consensus/src/builder/init_chain.rs @@ -32,8 +32,7 @@ impl Builder { let Self { app_state: Some(app_state), - identity_key: Some(identity_key), - extensions: _, + keys: _, } = self else { bail!("builder was not fully initialized") @@ -64,7 +63,6 @@ impl Builder { consensus, height: block::Height::from(0_u8), last_app_hash: app_hash.as_bytes().to_owned(), - identity_key: identity_key.try_into()?, }) } diff --git a/crates/test/mock-consensus/src/keyring.rs b/crates/test/mock-consensus/src/keyring.rs new file mode 100644 index 0000000000..e3fb6b4e9d --- /dev/null +++ b/crates/test/mock-consensus/src/keyring.rs @@ -0,0 +1,24 @@ +pub struct Keys { + pub consensus_signing_key: ed25519_consensus::SigningKey, + pub consensus_verification_key: ed25519_consensus::VerificationKey, +} + +impl Keys { + pub fn generate() -> Self { + Self::generate_with(rand_core::OsRng) + } + + /// Generates a set of keys using the provided random number generator. + pub fn generate_with(rng: R) -> Self + where + R: rand_core::RngCore + rand_core::CryptoRng, + { + let consensus_signing_key = ed25519_consensus::SigningKey::new(rng); + let consensus_verification_key = consensus_signing_key.verification_key(); + + Self { + consensus_signing_key, + consensus_verification_key, + } + } +} diff --git a/crates/test/mock-consensus/src/lib.rs b/crates/test/mock-consensus/src/lib.rs index 9fc4a138e2..9ef65dcb2f 100644 --- a/crates/test/mock-consensus/src/lib.rs +++ b/crates/test/mock-consensus/src/lib.rs @@ -6,14 +6,10 @@ pub mod block; pub mod builder; +pub mod keyring; mod abci; -use { - decaf377_rdsa::{SpendAuth, VerificationKey}, - tendermint::block::Height, -}; - /// A test node. /// /// Construct a new test node by calling [`TestNode::builder()`]. Use [`TestNode::block()`] to @@ -26,8 +22,7 @@ use { pub struct TestNode { consensus: C, last_app_hash: Vec, - height: Height, - identity_key: VerificationKey, + height: tendermint::block::Height, } impl TestNode { @@ -44,9 +39,4 @@ impl TestNode { // - https://doc.rust-lang.org/std/fmt/#formatting-traits format!("{:02X?}", self.last_app_hash) } - - /// Returns this test node's identity key. - pub fn identity_key(&self) -> VerificationKey { - self.identity_key - } }