Skip to content

Commit

Permalink
mock-consensus: 🌱 a genesis validator can be generated (#3902)
Browse files Browse the repository at this point in the history
fixes #3816. see #3588. this also addresses part of #3934.

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.

* #3588
* #3816

---------

Co-authored-by: Henry de Valence <hdevalence@penumbralabs.xyz>
  • Loading branch information
cratelyn and hdevalence authored Mar 7, 2024
1 parent d6679fc commit 60e2bd6
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 39 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
1 change: 1 addition & 0 deletions crates/core/app/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub struct App {
}

impl App {
/// Constructs a new application, using the provided [`Snapshot`].
pub async fn new(snapshot: Snapshot) -> Result<Self> {
tracing::debug!("initializing App instance");

Expand Down
44 changes: 17 additions & 27 deletions crates/core/app/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,24 @@
// 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},
pub use self::test_node_builder_ext::BuilderExt;

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,
};
use penumbra_genesis::AppState;
use penumbra_mock_consensus::TestNode;
use std::ops::Deref;

/// 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 {
Expand Down Expand Up @@ -80,22 +89,3 @@ impl TempStorageExt for TempStorage {
self.apply_genesis(Default::default()).await
}
}

/// Penumbra-specific extensions to the mock consensus builder.
pub trait BuilderExt: Sized {
type Error;
fn with_penumbra_auto_app_state(self, app_state: AppState) -> Result<Self, Self::Error>;
}

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))
}
}
118 changes: 118 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,118 @@
use {
penumbra_genesis::AppState,
penumbra_mock_consensus::builder::Builder,
penumbra_proto::{
core::keys::v1::{GovernanceKey, IdentityKey},
penumbra::core::component::stake::v1::Validator as PenumbraValidator,
},
};

/// 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> {
let Self { keyring, .. } = &self;

let app_state = if keyring.is_empty() {
// If there are no consensus keys to inject, pass along the provided app state.
app_state
} else {
// Otherwise, generate a penumbra validator for each entry in the keyring...
let validators = keyring
.verification_keys()
.cloned()
.map(generate_penumbra_validator)
.inspect(log_validator)
.collect::<Vec<_>>();
// ...and then inject these validators into the app state.
inject_penumbra_validators(app_state, validators)?
};

// 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 inject_penumbra_validators(
app_state: AppState,
validators: Vec<PenumbraValidator>,
) -> Result<AppState, anyhow::Error> {
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...
let overwritten = std::mem::replace(&mut content.stake_content.validators, validators);
// ...and 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 consensus verification key.
fn generate_penumbra_validator(
consensus_key: ed25519_consensus::VerificationKey,
) -> PenumbraValidator {
/// A temporary stub for validator keys.
///
/// An invalid key is intentionally provided here, until we have test coverage exercising the
/// use of these keys. Once we need it we will:
/// - generate a random signing key
/// - get its verification key
/// - use that for the identity key
/// - throw the signing key away
///
/// 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 INVALID_KEY_BYTES: [u8; 32] = [0; 32];

PenumbraValidator {
identity_key: Some(IdentityKey {
ik: INVALID_KEY_BYTES.to_vec().clone(),
}),
governance_key: Some(GovernanceKey {
gk: INVALID_KEY_BYTES.to_vec().clone(),
}),
consensus_key: consensus_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"
)
}
37 changes: 37 additions & 0 deletions crates/core/app/tests/mock_consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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 +38,42 @@ 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 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.
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 }
ed25519-consensus = { workspace = true }
rand_core = { workspace = true }
tap = { workspace = true }
tendermint = { workspace = true }
tower = { workspace = true, features = ["full"] }
Expand Down
24 changes: 14 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,16 @@
/// Most importantly, defines [`Builder::init_chain()`].
mod init_chain;

use {crate::TestNode, bytes::Bytes};
use {
crate::{keyring::Keyring, TestNode},
bytes::Bytes,
};

/// 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 keyring: Keyring,
}

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

/// Generates a single set of validator keys.
pub fn single_validator(self) -> Self {
Self {
keyring: Keyring::new_with_size(1),
..self
}
}
}
1 change: 1 addition & 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,7 @@ impl Builder {

let Self {
app_state: Some(app_state),
keyring: _,
} = self
else {
bail!("builder was not fully initialized")
Expand Down
Loading

0 comments on commit 60e2bd6

Please sign in to comment.