Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pd): support archives for migrate and join #4055

Merged
merged 1 commit into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion crates/bin/pd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ decaf377 = { workspace = true, features = ["parallel"],
decaf377-rdsa = { workspace = true }
directories = { workspace = true }
ed25519-consensus = { workspace = true }
flate2 = "1.0.28"
fs_extra = "1.3.0"
futures = { workspace = true }
hex = { workspace = true }
Expand Down Expand Up @@ -91,12 +92,13 @@ rand = { workspace = true }
rand_chacha = { workspace = true }
rand_core = { workspace = true, features = ["getrandom"] }
regex = { workspace = true }
reqwest = { version = "0.11", features = ["json"] }
reqwest = { version = "0.11", features = ["json", "stream"] }
rocksdb = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_with = { workspace = true, features = ["hex"] }
sha2 = { workspace = true }
tar = "0.4.40"
tempfile = { workspace = true }
tendermint = { workspace = true }
tendermint-config = { workspace = true }
Expand Down
26 changes: 21 additions & 5 deletions crates/bin/pd/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,23 +113,32 @@ pub enum RootCommand {
/// The home directory of the full node.
#[clap(long, env = "PENUMBRA_PD_HOME", display_order = 100)]
home: PathBuf,
/// The directory that the exported state will be written to.
/// The directory where the exported node state will be written.
#[clap(long, display_order = 200, alias = "export-path")]
export_directory: PathBuf,
/// An optional filepath for a compressed archive containing the exported
/// node state, e.g. ~/pd-backup.tar.gz.
#[clap(long, display_order = 200)]
export_path: PathBuf,
export_archive: Option<PathBuf>,
/// Whether to prune the JMT tree.
#[clap(long, display_order = 300)]
prune: bool,
},
/// Run a migration on the exported storage state of the full node,
/// and create a genesis file.
Migrate {
/// The directory containing exported state to which the upgrade will be applied.
#[clap(long, display_order = 200)]
target_dir: PathBuf,
/// The directory containing exported state, created via `pd export`, to be modified
/// in-place. This should be a pd home directory, with a subdirectory called "rocksdb".
#[clap(long, display_order = 200, alias = "target-dir")]
target_directory: PathBuf,
#[clap(long, display_order = 300)]
/// Timestamp of the genesis file in RFC3339 format. If unset, defaults to the current time,
/// unless the migration logic overrides it.
genesis_start: Option<tendermint::time::Time>,
/// An optional filepath for a compressed archive containing the migrated node state,
/// e.g. ~/pd-state-post-upgrade.tar.gz.
#[clap(long, display_order = 400)]
migrate_archive: Option<PathBuf>,
},
}

Expand Down Expand Up @@ -197,6 +206,13 @@ pub enum TestnetCommand {
default_value = "https://rpc.testnet.penumbra.zone"
)]
node: Url,

/// Optional URL of archived node state in .tar.gz format. The archive will be
/// downloaded and extracted locally, allowing the node to join a network at a block height
/// higher than 0.
#[clap(long)]
archive_url: Option<Url>,

/// Human-readable name to identify node on network
// Default: 'node-#'
#[clap(long, env = "PENUMBRA_PD_TM_MONIKER")]
Expand Down
91 changes: 66 additions & 25 deletions crates/bin/pd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ async fn main() -> anyhow::Result<()> {
tn_cmd:
TestnetCommand::Join {
node,
archive_url,
moniker,
external_address,
tendermint_rpc_bind,
Expand Down Expand Up @@ -290,14 +291,19 @@ async fn main() -> anyhow::Result<()> {
// Join the target testnet, looking up network info and writing
// local configs for pd and tendermint.
testnet_join(
output_dir,
output_dir.clone(),
node,
&node_name,
external_address,
tendermint_rpc_bind,
tendermint_p2p_bind,
)
.await?;

// Download and extract archive URL, if set.
if let Some(archive_url) = archive_url {
pd::testnet::join::unpack_state_archive(archive_url, output_dir).await?;
}
}

RootCommand::Testnet {
Expand Down Expand Up @@ -379,44 +385,79 @@ async fn main() -> anyhow::Result<()> {
t.write_configs()?;
}
RootCommand::Export {
mut home,
mut export_path,
home,
export_directory,
export_archive,
prune,
} => {
use fs_extra;

tracing::info!("exporting state to {}", export_path.display());
// Export state as directory.
let src_rocksdb_dir = home.join("rocksdb");
tracing::info!(
"copying node state {} -> {}",
src_rocksdb_dir.display(),
export_directory.display()
);
std::fs::create_dir_all(&export_directory)?;
let copy_opts = fs_extra::dir::CopyOptions::new();
home.push("rocksdb");
let from = [home.as_path()];
tracing::info!(?home, ?export_path, "copying from data dir to export dir",);
std::fs::create_dir_all(&export_path)?;
fs_extra::copy_items(&from, export_path.as_path(), &copy_opts)?;

tracing::info!("done copying");
if !prune {
return Ok(());
fs_extra::copy_items(
&[src_rocksdb_dir.as_path()],
export_directory.as_path(),
&copy_opts,
)?;
tracing::info!("finished copying node state");

let dst_rocksdb_dir = export_directory.join("rocksdb");
// If prune=true, then export-directory is required, because we must munge state prior
// to compressing. So we'll just mandate the presence of the --export-directory arg
// always.
if prune {
tracing::info!("pruning JMT tree");
let export = Storage::load(dst_rocksdb_dir, SUBSTORE_PREFIXES.to_vec()).await?;
let _ = StateDelta::new(export.latest_snapshot());
// TODO:
// - add utilities in `cnidarium` to prune a tree
// - apply the delta to the exported storage
// - apply checks: root hash, size, etc.
todo!()
}

tracing::info!("pruning JMT tree");
export_path.push("rocksdb");
let export = Storage::load(export_path, SUBSTORE_PREFIXES.to_vec()).await?;
let _ = StateDelta::new(export.latest_snapshot());
// TODO:
// - add utilities in `cnidarium` to prune a tree
// - apply the delta to the exported storage
// - apply checks: root hash, size, etc.
todo!()
// Compress to tarball if requested.
if let Some(archive_filepath) = export_archive {
pd::migrate::archive_directory(
dst_rocksdb_dir.clone(),
archive_filepath.clone(),
Some("rocksdb".to_owned()),
)?;
tracing::info!("export complete: {}", archive_filepath.display());
} else {
// Provide friendly "OK" message that's still accurate without archiving.
tracing::info!("export complete: {}", export_directory.display());
}
}
RootCommand::Migrate {
target_dir,
target_directory,
genesis_start,
migrate_archive,
} => {
tracing::info!("migrating state from {}", target_dir.display());
tracing::info!("migrating state in {}", target_directory.display());
SimpleMigration
.migrate(target_dir.clone(), genesis_start)
.migrate(target_directory.clone(), genesis_start)
.await
.context("failed to upgrade state")?;
// Compress to tarball if requested.
if let Some(archive_filepath) = migrate_archive {
pd::migrate::archive_directory(
target_directory.clone(),
archive_filepath.clone(),
None,
)?;
tracing::info!("migration complete: {}", archive_filepath.display());
} else {
// Provide friendly "OK" message that's still accurate without archiving.
tracing::info!("migration complete: {}", target_directory.display());
}
}
}
Ok(())
Expand Down
48 changes: 41 additions & 7 deletions crates/bin/pd/src/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! node operators must coordinate to perform a chain upgrade.
//! This module declares how local `pd` state should be altered, if at all,
//! in order to be compatible with the network post-chain-upgrade.
use anyhow::Context;
use std::path::PathBuf;

use cnidarium::{StateDelta, StateWrite, Storage};
Expand All @@ -16,6 +17,10 @@ use penumbra_stake::{

use crate::testnet::generate::TestnetConfig;

use flate2::write::GzEncoder;
use flate2::Compression;
use std::fs::File;

/// The kind of migration that should be performed.
pub enum Migration {
/// No-op migration.
Expand All @@ -36,9 +41,8 @@ impl Migration {
match self {
Migration::Noop => (),
Migration::SimpleMigration => {
let mut db_path = path_to_export.clone();
db_path.push("rocksdb");
let storage = Storage::load(db_path, SUBSTORE_PREFIXES.to_vec()).await?;
let rocksdb_dir = path_to_export.join("rocksdb");
let storage = Storage::load(rocksdb_dir, SUBSTORE_PREFIXES.to_vec()).await?;
let export_state = storage.latest_snapshot();
let root_hash = export_state.root_hash().await.expect("can get root hash");
let app_hash_pre_migration: RootHash = root_hash.into();
Expand Down Expand Up @@ -97,12 +101,10 @@ impl Migration {

let genesis_json = serde_json::to_string(&genesis).expect("can serialize genesis");
tracing::info!("genesis: {}", genesis_json);
let mut genesis_path = path_to_export.clone();
genesis_path.push("genesis.json");
let genesis_path = path_to_export.join("genesis.json");
std::fs::write(genesis_path, genesis_json).expect("can write genesis");

let mut validator_state_path = path_to_export.clone();
validator_state_path.push("priv_validator_state.json");
let validator_state_path = path_to_export.join("priv_validator_state.json");
let fresh_validator_state =
crate::testnet::generate::TestnetValidator::initial_state();
std::fs::write(validator_state_path, fresh_validator_state)
Expand All @@ -113,3 +115,35 @@ impl Migration {
Ok(())
}
}

/// Compress single directory to gzipped tar archive. Accepts an Option for naming
/// the subdir within the tar archive, which defaults to ".", meaning no nesting.
pub fn archive_directory(
src_directory: PathBuf,
archive_filepath: PathBuf,
subdir_within_archive: Option<String>,
) -> anyhow::Result<()> {
// Don't clobber an existing target archive.
if archive_filepath.exists() {
tracing::error!(
"export archive filepath already exists: {}",
archive_filepath.display()
);
anyhow::bail!("refusing to overwrite existing archive");
}

tracing::info!(
"creating archive {} -> {}",
src_directory.display(),
archive_filepath.display()
);
let tarball_file = File::create(&archive_filepath)
.context("failed to create file for archive: check parent directory and permissions")?;
let enc = GzEncoder::new(tarball_file, Compression::default());
let mut tarball = tar::Builder::new(enc);
let subdir_within_archive = subdir_within_archive.unwrap_or(String::from("."));
tarball
.append_dir_all(subdir_within_archive, src_directory.as_path())
.context("failed to package archive contents")?;
Ok(())
}
Loading
Loading