Skip to content

Commit

Permalink
mock-consensus: 🔍 rework validator generation (review)
Browse files Browse the repository at this point in the history
for context, see:

- #3902 (comment)
- #3902 (comment)

Co-authored-by: Henry de Valence <[email protected]>
  • Loading branch information
cratelyn and hdevalence committed Mar 1, 2024
1 parent 85dc813 commit 3e8691d
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 185 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

141 changes: 8 additions & 133 deletions crates/core/app/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -51,7 +56,7 @@ pub async fn start_test_node(storage: &TempStorage) -> anyhow::Result<PenumbraTe
let app_state = AppState::default();
let consensus = Consensus::new(storage.as_ref().clone());
TestNode::builder()
.with_penumbra_single_validator()
.single_validator()
.with_penumbra_auto_app_state(app_state)?
.init_chain(consensus)
.await
Expand Down Expand Up @@ -84,133 +89,3 @@ impl TempStorageExt for TempStorage {
self.apply_genesis(Default::default()).await
}
}

/// 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<Self, Self::Error>;
/// 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<Self, Self::Error> {
use penumbra_proto::penumbra::core::component::stake::v1 as pb;

// Take the list of genesis validators from the builder...
let validators = self
.extensions
.get::<Vec<pb::Validator>>()
.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<C> TestNodeExt for TestNode<C> {
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")
}
}
120 changes: 120 additions & 0 deletions crates/core/app/tests/common/test_node_builder_ext.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Self::Error>;
}

impl BuilderExt for Builder {
type Error = anyhow::Error;
fn with_penumbra_auto_app_state(self, app_state: AppState) -> Result<Self, Self::Error> {
// 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<V>(
app_state: AppState,
validators: V,
) -> Result<AppState, anyhow::Error>
where
V: IntoIterator<Item = PenumbraValidator>,
{
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"
)
}
32 changes: 20 additions & 12 deletions crates/core/app/tests/mock_consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
mod common;

use {
self::common::TestNodeExt as _,
anyhow::anyhow,
cnidarium::TempStorage,
penumbra_keys::test_keys,
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions crates/test/mock-consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
Loading

0 comments on commit 3e8691d

Please sign in to comment.