Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- [BREAKING] Renamed `NoteRoot` protobuf message used in `GetNoteScriptByRoot` gRPC endpoints into `NoteScriptRoot` ([#1722](https://github.com/0xMiden/node/pull/1722)).
- [BREAKING] Modified `TransactionHeader` serialization to allow converting back into the native type after serialization ([#1759](https://github.com/0xMiden/node/issues/1759)).
- Removed `chain_tip` requirement from mempool subscription request ([#1771](https://github.com/0xMiden/node/pull/1771)).
- Moved bootstrap procedure to `miden-node validator bootstrap` command ([#1764](https://github.com/0xMiden/node/pull/1764)).

### Fixes

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

21 changes: 13 additions & 8 deletions bin/node/src/commands/bundled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,21 @@ impl BundledCommand {
genesis_config_file,
validator_key,
} => {
// Currently the bundled bootstrap is identical to the store's bootstrap.
crate::commands::store::StoreCommand::Bootstrap {
data_directory,
accounts_directory,
genesis_config_file,
// Run validator bootstrap to create genesis block + account files.
crate::commands::validator::ValidatorCommand::bootstrap_genesis(
&data_directory,
&accounts_directory,
genesis_config_file.as_ref(),
validator_key,
}
.handle()
)
.await
.context("failed to bootstrap the store component")
.context("failed to bootstrap genesis block")?;

// Feed the genesis block file into the store bootstrap.
let genesis_block_path =
data_directory.join(crate::commands::validator::GENESIS_BLOCK_FILENAME);
crate::commands::store::bootstrap_store(&data_directory, &genesis_block_path)
.context("failed to bootstrap the store component")
},
BundledCommand::Start {
rpc_url,
Expand Down
148 changes: 24 additions & 124 deletions bin/node/src/commands/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ use std::path::{Path, PathBuf};

use anyhow::Context;
use miden_node_store::Store;
use miden_node_store::genesis::config::{AccountFileWithName, GenesisConfig};
use miden_node_store::genesis::GenesisBlock;
use miden_node_utils::clap::GrpcOptionsInternal;
use miden_node_utils::fs::ensure_empty_directory;
use miden_node_utils::grpc::UrlExt;
use miden_node_utils::signer::BlockSigner;
use miden_node_validator::ValidatorSigner;
use miden_protocol::block::ProvenBlock;
use miden_protocol::utils::Deserializable;
use url::Url;

use super::{
Expand All @@ -15,35 +16,21 @@ use super::{
ENV_STORE_NTX_BUILDER_URL,
ENV_STORE_RPC_URL,
};
use crate::commands::{
ENV_BLOCK_PROVER_URL,
ENV_ENABLE_OTEL,
ENV_GENESIS_CONFIG_FILE,
ValidatorKey,
};
use crate::commands::{ENV_BLOCK_PROVER_URL, ENV_ENABLE_OTEL};

#[expect(clippy::large_enum_variant, reason = "single use enum")]
#[derive(clap::Subcommand)]
pub enum StoreCommand {
/// Bootstraps the blockchain database with the genesis block.
///
/// The genesis block contains a single public faucet account. The private key for this
/// account is written to the `accounts-directory` which can be used to control the account.
/// Bootstraps the blockchain database with a pre-existing genesis block.
///
/// This key is not required by the node and can be moved.
/// The genesis block file should be produced by `miden-node validator bootstrap`.
Bootstrap {
/// Directory in which to store the database and raw block data.
#[arg(long, env = ENV_DATA_DIRECTORY, value_name = "DIR")]
data_directory: PathBuf,
/// Directory to write the account data to.
#[arg(long, value_name = "DIR")]
accounts_directory: PathBuf,
/// Use the given configuration file to construct the genesis state from.
#[arg(long, env = ENV_GENESIS_CONFIG_FILE, value_name = "GENESIS_CONFIG")]
genesis_config_file: Option<PathBuf>,
/// Configuration for the Validator key used to sign genesis block.
#[command(flatten)]
validator_key: ValidatorKey,
/// Path to the pre-signed genesis block file produced by the validator.
#[arg(long, value_name = "FILE")]
genesis_block: PathBuf,
},

/// Starts the store component.
Expand Down Expand Up @@ -84,22 +71,12 @@ pub enum StoreCommand {
}

impl StoreCommand {
/// Executes the subcommand as described by each variants documentation.
/// Executes the subcommand as described by each variant's documentation.
pub async fn handle(self) -> anyhow::Result<()> {
match self {
StoreCommand::Bootstrap {
data_directory,
accounts_directory,
genesis_config_file,
validator_key,
} => {
Self::bootstrap(
&data_directory,
&accounts_directory,
genesis_config_file.as_ref(),
validator_key,
)
.await
StoreCommand::Bootstrap { data_directory, genesis_block } => {
ensure_empty_directory(&data_directory)?;
bootstrap_store(&data_directory, &genesis_block)
},
StoreCommand::Start {
rpc_url,
Expand Down Expand Up @@ -172,93 +149,16 @@ impl StoreCommand {
.await
.context("failed while serving store component")
}
}

async fn bootstrap(
data_directory: &Path,
accounts_directory: &Path,
genesis_config: Option<&PathBuf>,
validator_key: ValidatorKey,
) -> anyhow::Result<()> {
// Parse genesis config (or default if not given).
let config = genesis_config
.map(|file_path| {
GenesisConfig::read_toml_file(file_path).with_context(|| {
format!("failed to parse genesis config from file {}", file_path.display())
})
})
.transpose()?
.unwrap_or_default();

// Create directories if they do not already exist.
for directory in &[accounts_directory, data_directory] {
if fs_err::exists(directory)? {
let is_empty = fs_err::read_dir(directory)?.next().is_none();
// If the directory exists and is empty, we store the files there
if !is_empty {
anyhow::bail!(format!("{} exists but it is not empty.", directory.display()));
}
} else {
fs_err::create_dir(directory).with_context(|| {
format!(
"failed to create {} at {}",
directory
.file_name()
.unwrap_or(std::ffi::OsStr::new("directory"))
.display(),
directory.display()
)
})?;
}
}

// Bootstrap with KMS key or local key.
let signer = validator_key.into_signer().await?;
match signer {
ValidatorSigner::Kms(signer) => {
Self::bootstrap_accounts_and_store(
config,
signer,
accounts_directory,
data_directory,
)
.await
},
ValidatorSigner::Local(signer) => {
Self::bootstrap_accounts_and_store(
config,
signer,
accounts_directory,
data_directory,
)
.await
},
}
}

/// Builds the genesis state of the chain, writes accounts to file, and bootstraps the store.
async fn bootstrap_accounts_and_store(
config: GenesisConfig,
signer: impl BlockSigner,
accounts_directory: &Path,
data_directory: &Path,
) -> anyhow::Result<()> {
// Build genesis state with the provided signer.
let (genesis_state, secrets) = config.into_state(signer)?;

// Write accounts to file.
for item in secrets.as_account_files(&genesis_state) {
let AccountFileWithName { account_file, name } = item?;
let accountpath = accounts_directory.join(name);
// do not override existing keys
fs_err::OpenOptions::new()
.create_new(true)
.write(true)
.open(&accountpath)
.context("key file already exists")?;
account_file.write(accountpath)?;
}
/// Reads a genesis block from disk, validates it, and bootstraps the store.
pub fn bootstrap_store(data_directory: &Path, genesis_block_path: &Path) -> anyhow::Result<()> {
// Read and deserialize the genesis block file.
let bytes = fs_err::read(genesis_block_path).context("failed to read genesis block")?;
let proven_block = ProvenBlock::read_from_bytes(&bytes)
.context("failed to deserialize genesis block from file")?;
let genesis_block =
GenesisBlock::try_from(proven_block).context("genesis block validation failed")?;

// Bootstrap store.
Store::bootstrap(genesis_state, data_directory).await
}
Store::bootstrap(&genesis_block, data_directory)
}
Loading
Loading