Skip to content

Commit

Permalink
mock-consensus: 🌱 define genesis validator
Browse files Browse the repository at this point in the history
  • Loading branch information
cratelyn committed Feb 28, 2024
1 parent c078784 commit 65e83fc
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 26 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/core/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ tracing = {workspace = true}
ed25519-consensus = {workspace = true}
penumbra-mock-consensus = {workspace = true}
penumbra-mock-client = {workspace = true}
rand = {workspace = true}
rand_core = {workspace = true}
rand_chacha = {workspace = true}
tap = {workspace = true}
Expand Down
151 changes: 135 additions & 16 deletions crates/core/app/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -47,7 +51,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()
.single_validator()
.with_penumbra_single_validator()
.with_penumbra_auto_app_state(app_state)?
.init_chain(consensus)
.await
Expand Down Expand Up @@ -83,19 +87,134 @@ 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<Self, Self::Error>;
/// Creates a single validator with a randomly generated 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> {
// 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::<Vec<_>>()
.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))
}

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().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 = 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
}
}
}

pub trait TestNodeExt {
fn penumbra_identity_key(&self) -> penumbra_stake::IdentityKey;
}

impl<C> TestNodeExt for TestNode<C> {
// TODO(kate): `penumbra_stake::IdentityKey` could be `From<[u8; 32]>`
fn penumbra_identity_key(&self) -> penumbra_stake::IdentityKey {
use penumbra_proto::core::keys::v1 as pb;
use prost::Message;
pb::IdentityKey::decode(self.identity_key().as_slice())
.unwrap()
.try_into()
.unwrap()
}
}
29 changes: 29 additions & 0 deletions crates/core/app/tests/mock_consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
mod common;

use {
self::common::TestNodeExt as _,
anyhow::anyhow,
cnidarium::TempStorage,
penumbra_ibc::component::ClientStateReadExt as _,
penumbra_keys::test_keys,
penumbra_mock_client::MockClient,
penumbra_mock_consensus::TestNode,
penumbra_proto::DomainType,
penumbra_sct::component::{clock::EpochRead, tree::SctRead as _},
penumbra_shielded_pool::{OutputPlan, SpendPlan},
penumbra_stake::component::validator_handler::ValidatorDataRead as _,
penumbra_transaction::{
memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan,
},
Expand All @@ -37,6 +40,32 @@ 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.penumbra_identity_key();
match storage
.latest_snapshot()
.get_validator_state(&identity_key)
.await?
.ok_or_else(|| anyhow!("genesis validator state was not found"))?
{
penumbra_stake::validator::State::Active => {}
other => panic!("unexpected genesis validator state, found: {other}"),
}

// 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]
Expand Down
6 changes: 6 additions & 0 deletions crates/test/mock-consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
17 changes: 7 additions & 10 deletions crates/test/mock-consensus/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
/// 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<Bytes>,
pub app_state: Option<Bytes>,
pub validators: Vec<pb::Validator>,
pub identity_key: [u8; 32],
}

impl TestNode<()> {
Expand All @@ -21,14 +26,6 @@ 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
}

/// Sets the `app_state_bytes` to send the ABCI application upon chain initialization.
pub fn app_state(self, app_state: impl Into<Bytes>) -> Self {
let app_state = Some(app_state.into());
Expand Down
3 changes: 3 additions & 0 deletions crates/test/mock-consensus/src/builder/init_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ impl Builder {

let Self {
app_state: Some(app_state),
identity_key,
validators: _,
} = self
else {
bail!("builder was not fully initialized")
Expand Down Expand Up @@ -62,6 +64,7 @@ impl Builder {
consensus,
height: block::Height::from(0_u8),
last_app_hash: app_hash.as_bytes().to_owned(),
identity_key,
})
}

Expand Down
6 changes: 6 additions & 0 deletions crates/test/mock-consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct TestNode<C> {
consensus: C,
last_app_hash: Vec<u8>,
height: tendermint::block::Height,
identity_key: [u8; 32],
}

impl<C> TestNode<C> {
Expand All @@ -38,4 +39,9 @@ impl<C> TestNode<C> {
// - 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) -> &[u8; 32] {
&self.identity_key
}
}

0 comments on commit 65e83fc

Please sign in to comment.