Skip to content

Commit

Permalink
chain: support checkpoints in the genesis state (#3034)
Browse files Browse the repository at this point in the history
Closes #1809, and prepares #3004 since we will be able to match the supplied genesis state to decide whether we want to run chain initialization logic e.g. setting the genesis base rate, or fast-forward to committing the initial height block.

This PR turns `genesis::AppState` into an enum:

```rust
pub enum AppState {
    Content(/* a full genesis configuration */),
    Checkpoint(/* a free-form byte string e.g. an apphash, a commit hash */)
}
```

and percolate those changes across the codebase.
  • Loading branch information
erwanor authored Sep 15, 2023
1 parent 2e51199 commit dfb721c
Show file tree
Hide file tree
Showing 13 changed files with 510 additions and 190 deletions.
25 changes: 16 additions & 9 deletions crates/bin/pd/src/testnet/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ impl TestnetConfig {
testnet_validators.iter().map(|v| v.try_into()).collect();
let validators = validators?;

let app_state = Self::make_appstate(
let app_state = Self::make_genesis_content(
chain_id,
allocations,
validators.to_vec(),
Expand Down Expand Up @@ -172,18 +172,19 @@ impl TestnetConfig {
}
}

/// Build initial state for Penumbra application, for inclusion in Tendermint genesis.
fn make_appstate(
/// Create a full genesis configuration for inclusion in the tendermint
/// genesis config.
fn make_genesis_content(
chain_id: &str,
allocations: Vec<Allocation>,
validators: Vec<Validator>,
active_validator_limit: Option<u64>,
epoch_duration: Option<u64>,
unbonding_epochs: Option<u64>,
) -> anyhow::Result<genesis::AppState> {
) -> anyhow::Result<genesis::Content> {
// Look up default chain params, so we can fill in defaults.
let default_params = ChainParameters::default();
let app_state = genesis::AppState {
let app_state = genesis::Content {
allocations: allocations.clone(),
chain_params: ChainParameters {
chain_id: chain_id.to_string(),
Expand All @@ -202,7 +203,7 @@ impl TestnetConfig {

/// Build Tendermint genesis data, based on Penumbra initial application state.
pub(crate) fn make_genesis(
app_state: genesis::AppState,
app_state: genesis::Content,
) -> anyhow::Result<Genesis<genesis::AppState>> {
// Use now as genesis time
let genesis_time = Time::from_unix_timestamp(
Expand Down Expand Up @@ -245,7 +246,7 @@ impl TestnetConfig {
},
// always empty in genesis json
app_hash: tendermint::AppHash::default(),
app_state,
app_state: genesis::AppState::Content(app_state),
// Set empty validator set for Tendermint config, which falls back to reading
// validators from the AppState, via ResponseInitChain:
// https://docs.tendermint.com/v0.32/tendermint-core/using-tendermint.html
Expand Down Expand Up @@ -627,7 +628,10 @@ mod tests {
assert_eq!(testnet_config.name, "test-chain-1234");
assert_eq!(testnet_config.genesis.validators.len(), 0);
// No external address template was given, so only 1 validator will be present.
assert_eq!(testnet_config.genesis.app_state.validators.len(), 1);
let genesis::AppState::Content(app_state) = testnet_config.genesis.app_state else {
unimplemented!("TODO: support checkpointed app state")
};
assert_eq!(app_state.validators.len(), 1);
Ok(())
}

Expand All @@ -650,7 +654,10 @@ mod tests {
)?;
assert_eq!(testnet_config.name, "test-chain-4567");
assert_eq!(testnet_config.genesis.validators.len(), 0);
assert_eq!(testnet_config.genesis.app_state.validators.len(), 2);
let genesis::AppState::Content(app_state) = testnet_config.genesis.app_state else {
unimplemented!("TODO: support checkpointed app state")
};
assert_eq!(app_state.validators.len(), 2);
Ok(())
}

Expand Down
4 changes: 2 additions & 2 deletions crates/bin/pd/src/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use penumbra_chain::{
component::{AppHash, StateReadExt},
genesis::AppState,
genesis::Content,
};
use penumbra_stake::StateReadExt as _;
use penumbra_storage::{StateDelta, StateWrite, Storage};
Expand Down Expand Up @@ -53,7 +53,7 @@ pub async fn migrate(path_to_export: PathBuf, upgrade: Upgrade) -> anyhow::Resul

/* ---------- genereate genesis ------------ */
let validators = migrated_state.validator_list().await?;
let mut app_state = AppState::default();
let mut app_state = Content::default();
app_state.chain_params = chain_params;
app_state.validators = validators.into_iter().map(Into::into).collect();
let mut genesis = TestnetConfig::make_genesis(app_state.clone()).unwrap();
Expand Down
14 changes: 10 additions & 4 deletions crates/core/app/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,13 @@ impl App {
events
}

pub async fn init_chain(&mut self, app_state: &genesis::AppState) {
pub async fn init_chain(&mut self, app_state_general: &genesis::AppState) {
let app_state = match app_state_general {
genesis::AppState::Checkpoint(_) => {
unimplemented!("adding support with init handshake pr")
}
genesis::AppState::Content(state) => state,
};
let mut state_tx = self
.state
.try_begin_transaction()
Expand Down Expand Up @@ -111,12 +117,12 @@ impl App {
},
);

Distributions::init_chain(&mut state_tx, app_state).await;
Staking::init_chain(&mut state_tx, app_state).await;
Distributions::init_chain(&mut state_tx, app_state_general).await;
Staking::init_chain(&mut state_tx, app_state_general).await;
IBCComponent::init_chain(&mut state_tx, &()).await;
Dex::init_chain(&mut state_tx, &()).await;
Governance::init_chain(&mut state_tx, &()).await;
ShieldedPool::init_chain(&mut state_tx, app_state).await;
ShieldedPool::init_chain(&mut state_tx, app_state_general).await;

// Create a synthetic height-zero block
App::finish_block(&mut state_tx).await;
Expand Down
1 change: 1 addition & 0 deletions crates/core/component/chain/src/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ mod app_state;

pub use allocation::Allocation;
pub use app_state::AppState;
pub use app_state::Content;
14 changes: 7 additions & 7 deletions crates/core/component/chain/src/genesis/allocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,29 @@ use serde::{Deserialize, Serialize};
/// A (transparent) genesis allocation.
#[derive(Clone, Serialize, Deserialize)]
#[serde(
try_from = "pb::genesis_app_state::Allocation",
into = "pb::genesis_app_state::Allocation"
try_from = "pb::genesis_content::Allocation",
into = "pb::genesis_content::Allocation"
)]
pub struct Allocation {
pub amount: Amount,
pub denom: String,
pub address: Address,
}

impl From<Allocation> for pb::genesis_app_state::Allocation {
impl From<Allocation> for pb::genesis_content::Allocation {
fn from(a: Allocation) -> Self {
pb::genesis_app_state::Allocation {
pb::genesis_content::Allocation {
amount: Some(a.amount.into()),
denom: a.denom,
address: Some(a.address.into()),
}
}
}

impl TryFrom<pb::genesis_app_state::Allocation> for Allocation {
impl TryFrom<pb::genesis_content::Allocation> for Allocation {
type Error = anyhow::Error;

fn try_from(msg: pb::genesis_app_state::Allocation) -> Result<Self, Self::Error> {
fn try_from(msg: pb::genesis_content::Allocation) -> Result<Self, Self::Error> {
Ok(Allocation {
amount: msg
.amount
Expand Down Expand Up @@ -61,5 +61,5 @@ impl TypeUrl for Allocation {
}

impl DomainType for Allocation {
type Proto = pb::genesis_app_state::Allocation;
type Proto = pb::genesis_content::Allocation;
}
62 changes: 55 additions & 7 deletions crates/core/component/chain/src/genesis/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ use crate::params::ChainParameters;
/// The application state at genesis.
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(try_from = "pb::GenesisAppState", into = "pb::GenesisAppState")]
pub struct AppState {
pub enum AppState {
/// The application state at genesis.
Content(Content),
/// The checkpointed application state at genesis, contains a free-form hash.
Checkpoint(Vec<u8>),
}

#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(try_from = "pb::GenesisContent", into = "pb::GenesisContent")]
pub struct Content {
/// Global configuration for the chain, such as chain ID and epoch duration.
pub chain_params: ChainParameters,
/// The initial validator set.
Expand All @@ -20,6 +29,12 @@ pub struct AppState {
}

impl Default for AppState {
fn default() -> Self {
Self::Content(Default::default())
}
}

impl Default for Content {
fn default() -> Self {
Self {
chain_params: Default::default(),
Expand Down Expand Up @@ -69,10 +84,25 @@ impl Default for AppState {

impl From<AppState> for pb::GenesisAppState {
fn from(a: AppState) -> Self {
let genesis_state = match a {
AppState::Content(c) => {
pb::genesis_app_state::GenesisAppState::GenesisContent(c.into())
}
AppState::Checkpoint(h) => pb::genesis_app_state::GenesisAppState::GenesisCheckpoint(h),
};

pb::GenesisAppState {
validators: a.validators.into_iter().map(Into::into).collect(),
allocations: a.allocations.into_iter().map(Into::into).collect(),
chain_params: Some(a.chain_params.into()),
genesis_app_state: Some(genesis_state),
}
}
}

impl From<Content> for pb::GenesisContent {
fn from(value: Content) -> Self {
pb::GenesisContent {
chain_params: Some(value.chain_params.into()),
validators: value.validators.into_iter().map(Into::into).collect(),
allocations: value.allocations.into_iter().map(Into::into).collect(),
}
}
}
Expand All @@ -81,7 +111,25 @@ impl TryFrom<pb::GenesisAppState> for AppState {
type Error = anyhow::Error;

fn try_from(msg: pb::GenesisAppState) -> Result<Self, Self::Error> {
Ok(AppState {
let state = msg
.genesis_app_state
.ok_or_else(|| anyhow::anyhow!("missing genesis_app_state field in proto"))?;
match state {
pb::genesis_app_state::GenesisAppState::GenesisContent(c) => {
Ok(AppState::Content(c.try_into()?))
}
pb::genesis_app_state::GenesisAppState::GenesisCheckpoint(h) => {
Ok(AppState::Checkpoint(h.try_into()?))
}
}
}
}

impl TryFrom<pb::GenesisContent> for Content {
type Error = anyhow::Error;

fn try_from(msg: pb::GenesisContent) -> Result<Self, Self::Error> {
Ok(Content {
chain_params: msg
.chain_params
.context("chain params not present in protobuf message")?
Expand Down Expand Up @@ -112,13 +160,13 @@ impl DomainType for AppState {
#[cfg(test)]
mod test {
use super::*;
/// Check that the default implementation of AppState contains zero validators,
/// Check that the default implementation of contains zero validators,
/// requiring validators to be passed in out of band. N.B. there's also a
/// `validators` field in the [`tendermint::Genesis`] struct, which we don't use,
/// preferring the AppState definition instead.
#[test]
fn check_validator_defaults() -> anyhow::Result<()> {
let a = AppState {
let a = Content {
..Default::default()
};
assert!(a.validators.len() == 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ impl Component for ShieldedPool {

#[instrument(name = "shielded_pool", skip(state, app_state))]
async fn init_chain<S: StateWrite>(mut state: S, app_state: &genesis::AppState) {
let app_state = match app_state {
genesis::AppState::Checkpoint(_) => {
unimplemented!("adding support with init handshake pr")
}
genesis::AppState::Content(app_state) => app_state,
};

// Register a denom for each asset in the genesis state
for allocation in &app_state.allocations {
tracing::debug!(?allocation, "processing allocation");
Expand Down
7 changes: 7 additions & 0 deletions crates/core/component/stake/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,13 @@ impl Component for Staking {

#[instrument(name = "staking", skip(state, app_state))]
async fn init_chain<S: StateWrite>(mut state: S, app_state: &genesis::AppState) {
let app_state = match app_state {
genesis::AppState::Checkpoint(_) => {
unimplemented!("adding support with init handshake pr")
}
genesis::AppState::Content(state) => state,
};

let starting_height = state
.get_block_height()
.await
Expand Down
23 changes: 20 additions & 3 deletions crates/proto/src/gen/penumbra.core.chain.v1alpha1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,32 @@ pub struct SpendInfo {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GenesisAppState {
#[prost(oneof = "genesis_app_state::GenesisAppState", tags = "1, 2")]
pub genesis_app_state: ::core::option::Option<genesis_app_state::GenesisAppState>,
}
/// Nested message and enum types in `GenesisAppState`.
pub mod genesis_app_state {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum GenesisAppState {
#[prost(message, tag = "1")]
GenesisContent(super::GenesisContent),
#[prost(bytes, tag = "2")]
GenesisCheckpoint(::prost::alloc::vec::Vec<u8>),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GenesisContent {
#[prost(message, optional, tag = "1")]
pub chain_params: ::core::option::Option<ChainParameters>,
#[prost(message, repeated, tag = "2")]
pub validators: ::prost::alloc::vec::Vec<super::super::stake::v1alpha1::Validator>,
#[prost(message, repeated, tag = "3")]
pub allocations: ::prost::alloc::vec::Vec<genesis_app_state::Allocation>,
pub allocations: ::prost::alloc::vec::Vec<genesis_content::Allocation>,
}
/// Nested message and enum types in `GenesisAppState`.
pub mod genesis_app_state {
/// Nested message and enum types in `GenesisContent`.
pub mod genesis_content {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Allocation {
Expand Down
Loading

0 comments on commit dfb721c

Please sign in to comment.