Skip to content

Commit

Permalink
mock-consensus: 🌱 define genesis validator
Browse files Browse the repository at this point in the history
fixes #3816.

this provides extension facilities to the mock consensus test node
builder, which allows penumbra-app tests to define a single validator,
and subsequently retrieve it from the chain state.

see `mock_consensus_can_define_a_genesis_validator`. when run with
`--no-capture` enabled, these logs will be visible:

```
  2024-02-28T23:00:43.751036Z DEBUG penumbra_stake::component::stake: adding validator identity to consensus set index, validator: penumbravalid172v76yyqwngcln2dxrs8ht0sjgswer3569yyhezgsz6aj97ecvqqyf3h9h
    at crates/core/component/stake/src/component/stake.rs:533
    in penumbra_stake::component::stake::staking

[...]

  2024-02-28T23:00:43.776880Z  INFO penumbra_app::server::consensus: genesis state is a full configuration
    at crates/core/app/src/server/consensus.rs:145

  2024-02-28T23:00:43.780436Z DEBUG penumbra_app::app: finished committing state, jmt_root: RootHash("46dc0e9561f17eee61a2c13f517036d4d0a4c77c60362cb6cc165083675dcaf7")
    at crates/core/app/src/app/mod.rs:592
```

logging facilities are provided so that helper warnings should be given
to users that forget to call `with_penumbra_single_validator`, or
provide an `AppState` object whose validator list would be overwritten.

the `serde_json` dependency is removed from the mock consensus library,
it is no longer used.

a warning is added to the mock consensus library to note to future
contributors that other penumbra dependencies should be avoided in that
library.

a new `http::Extensions` field is added to the builder. it is not used
internally, but provides extension traits a place to hold additional
state.

* #3588
* #3816
  • Loading branch information
cratelyn committed Feb 29, 2024
1 parent c078784 commit 3499c2d
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 29 deletions.
4 changes: 3 additions & 1 deletion 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
147 changes: 131 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,130 @@ 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.
///
/// 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> {
// 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
.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")
}
}
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,6 +6,7 @@
mod common;

use {
self::common::TestNodeExt as _,
anyhow::anyhow,
cnidarium::TempStorage,
penumbra_keys::test_keys,
Expand All @@ -14,6 +15,7 @@ use {
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 +39,33 @@ 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)
.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}"),
}

// 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
7 changes: 6 additions & 1 deletion crates/test/mock-consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ repository.workspace = true
homepage.workspace = true
license.workspace = true

# NB: to avoid circular dependencies: do not add other `penumbra-*` crates
# as dependencies, below. we should provide interfaces in terms of generic
# types, and provide penumbra-specific helper functions via extension traits.

[dependencies]
anyhow = { workspace = true }
bytes = { workspace = true }
serde_json = { workspace = true }
decaf377-rdsa = { workspace = true }
http = { workspace = true }
tap = { workspace = true }
tendermint = { workspace = true }
tower = { workspace = true, features = ["full"] }
Expand Down
44 changes: 34 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,21 @@
/// Most importantly, defines [`Builder::init_chain()`].
mod init_chain;

use {crate::TestNode, bytes::Bytes};
use {
crate::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 {
app_state: Option<Bytes>,
pub app_state: Option<Bytes>,
pub identity_key: Option<VerificationKeyBytes<SpendAuth>>,
pub extensions: Extensions,
}

impl TestNode<()> {
Expand All @@ -21,17 +30,32 @@ 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());
Self { app_state, ..self }
}

/// Sets the test node's identity key.
pub fn identity_key(self, identity_key: impl Into<VerificationKeyBytes<SpendAuth>>) -> Self {
let identity_key = Some(identity_key.into());
Self {
identity_key,
..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<T>(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
}
}
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: Some(identity_key),
extensions: _,
} = 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: identity_key.try_into()?,
})
}

Expand Down
13 changes: 12 additions & 1 deletion crates/test/mock-consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ pub mod builder;

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
Expand All @@ -21,7 +26,8 @@ mod abci;
pub struct TestNode<C> {
consensus: C,
last_app_hash: Vec<u8>,
height: tendermint::block::Height,
height: Height,
identity_key: VerificationKey<SpendAuth>,
}

impl<C> TestNode<C> {
Expand All @@ -38,4 +44,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) -> VerificationKey<SpendAuth> {
self.identity_key
}
}

0 comments on commit 3499c2d

Please sign in to comment.