diff --git a/Cargo.lock b/Cargo.lock index 0b24a818e..b136137b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3325,7 +3325,6 @@ version = "0.15.0" dependencies = [ "anyhow", "clap", - "fs-err", "humantime", "miden-node-block-producer", "miden-node-proto", @@ -3550,6 +3549,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry_sdk", "rand 0.9.4", + "reqwest", "thiserror 2.0.18", "tokio", "tonic", @@ -3572,7 +3572,6 @@ dependencies = [ "build-rs", "clap", "diesel", - "fs-err", "futures", "humantime", "libsqlite3-sys", diff --git a/bin/genesis/README.md b/bin/genesis/README.md index 8b570b77f..5feafd48c 100644 --- a/bin/genesis/README.md +++ b/bin/genesis/README.md @@ -44,11 +44,13 @@ miden-validator bootstrap \ --genesis-config-file ./genesis/genesis.toml \ --validator.key.hex -# 3. Bootstrap the store -miden-node store bootstrap --data-directory ./data +# 3. Bootstrap the node +miden-node bootstrap \ + --data-directory ./node-data \ + --file ./data/genesis.dat # 4. Start the node -miden-node bundled start --data-directory ./data ... +miden-node sequencer --data-directory ./node-data ... ``` ## TODO diff --git a/bin/node/Cargo.toml b/bin/node/Cargo.toml index 314bdaa73..8e1f76958 100644 --- a/bin/node/Cargo.toml +++ b/bin/node/Cargo.toml @@ -20,7 +20,6 @@ tracing-forest = ["miden-node-block-producer/tracing-forest"] [dependencies] anyhow = { workspace = true } clap = { features = ["env", "string"], workspace = true } -fs-err = { workspace = true } humantime = { workspace = true } miden-node-block-producer = { workspace = true } miden-node-proto = { workspace = true } diff --git a/bin/node/src/commands/lifecycle.rs b/bin/node/src/commands/lifecycle.rs index f53ba833b..a4d5956aa 100644 --- a/bin/node/src/commands/lifecycle.rs +++ b/bin/node/src/commands/lifecycle.rs @@ -1,11 +1,16 @@ use std::path::{Path, PathBuf}; use anyhow::Context; +use clap::ArgGroup; use miden_node_store::genesis::GenesisBlock; use miden_node_store::{DataDirectory, Db, State}; use miden_node_utils::fs::ensure_empty_directory; +use miden_node_utils::genesis::{ + OfficialNetwork, + fetch_signed_genesis_block, + read_signed_genesis_block, +}; use miden_protocol::block::SignedBlock; -use miden_protocol::utils::serde::Deserializable; use super::ENV_DATA_DIRECTORY; @@ -13,28 +18,48 @@ use super::ENV_DATA_DIRECTORY; // ================================================================================================ #[derive(clap::Args, Clone, Debug)] +#[command(group( + ArgGroup::new("genesis_block_source") + .required(true) + .multiple(false) + .args(["genesis_block_file", "network"]) +))] pub struct BootstrapCommand { /// Directory to initialize with the node's local data storage. #[arg(long, env = ENV_DATA_DIRECTORY, value_name = "DIR")] data_directory: PathBuf, - /// Path to the trusted, signed genesis block file. - #[arg(long, value_name = "FILE")] - genesis_block: PathBuf, + /// Bootstrap from a trusted genesis block file. + #[arg(long = "file", value_name = "FILE")] + genesis_block_file: Option, + + /// Bootstrap for an official Miden network. + #[arg(long, value_enum, value_name = "NETWORK")] + network: Option, } impl BootstrapCommand { - pub fn handle(self) -> anyhow::Result<()> { + pub async fn handle(self) -> anyhow::Result<()> { ensure_empty_directory(&self.data_directory)?; - bootstrap_store(&self.data_directory, &self.genesis_block) + let signed_block = + read_bootstrap_genesis_block(self.genesis_block_file.as_deref(), self.network).await?; + bootstrap_store(&self.data_directory, signed_block) + } +} + +async fn read_bootstrap_genesis_block( + genesis_block_file: Option<&Path>, + network: Option, +) -> anyhow::Result { + match (genesis_block_file, network) { + (Some(path), None) => read_signed_genesis_block(path), + (None, Some(network)) => fetch_signed_genesis_block(network).await, + _ => unreachable!("clap requires exactly one genesis block source"), } } -/// 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<()> { - let bytes = fs_err::read(genesis_block_path).context("failed to read genesis block")?; - let signed_block = SignedBlock::read_from_bytes(&bytes) - .context("failed to deserialize genesis block from file")?; +/// Validates a signed genesis block and bootstraps the store. +pub fn bootstrap_store(data_directory: &Path, signed_block: SignedBlock) -> anyhow::Result<()> { let genesis_block = GenesisBlock::try_from(signed_block).context("genesis block validation failed")?; diff --git a/bin/node/src/commands/mod.rs b/bin/node/src/commands/mod.rs index e04db6375..66e23cd8d 100644 --- a/bin/node/src/commands/mod.rs +++ b/bin/node/src/commands/mod.rs @@ -66,7 +66,7 @@ impl Command { pub(crate) async fn execute(self) -> anyhow::Result<()> { match self { - Command::Bootstrap(bootstrap_command) => bootstrap_command.handle(), + Command::Bootstrap(bootstrap_command) => bootstrap_command.handle().await, Command::Migrate(migrate_command) => migrate_command.handle().await, Command::Sequencer(sequencer_command) => sequencer_command.handle().await, Command::Full(full_node_command) => full_node_command.handle().await, diff --git a/bin/ntx-builder/Cargo.toml b/bin/ntx-builder/Cargo.toml index d7f49ac2d..dec37c203 100644 --- a/bin/ntx-builder/Cargo.toml +++ b/bin/ntx-builder/Cargo.toml @@ -22,7 +22,6 @@ anyhow = { workspace = true } backon = { workspace = true } clap = { features = ["env", "string"], workspace = true } diesel = { features = ["numeric", "sqlite"], workspace = true } -fs-err = { workspace = true } futures = { workspace = true } humantime = { workspace = true } libsqlite3-sys = { workspace = true } diff --git a/bin/ntx-builder/src/commands/mod.rs b/bin/ntx-builder/src/commands/mod.rs index fc32efeeb..2a040a11c 100644 --- a/bin/ntx-builder/src/commands/mod.rs +++ b/bin/ntx-builder/src/commands/mod.rs @@ -4,12 +4,16 @@ use std::path::{Path, PathBuf}; use std::time::Duration; use anyhow::Context; -use clap::Parser; +use clap::{ArgGroup, Parser}; use miden_node_utils::clap::duration_to_human_readable_string; use miden_node_utils::fs::ensure_empty_directory; +use miden_node_utils::genesis::{ + OfficialNetwork, + fetch_signed_genesis_block, + read_signed_genesis_block, +}; use miden_node_utils::logging::OpenTelemetry; use miden_protocol::block::SignedBlock; -use miden_protocol::utils::serde::Deserializable; use tokio::net::TcpListener; use tonic::metadata::AsciiMetadataValue; use url::Url; @@ -128,14 +132,24 @@ pub enum NtxBuilderCommand { /// /// This must be run once before `start` so that the database always contains at least the /// genesis block. + #[command(group( + ArgGroup::new("genesis_block_source") + .required(true) + .multiple(false) + .args(["genesis_block_file", "network"]) + ))] Bootstrap { /// Directory for the ntx-builder's persistent database. #[arg(long = "data-directory", env = ENV_DATA_DIRECTORY, value_name = "DIR")] data_directory: PathBuf, - /// Path to the trusted, signed genesis block file. - #[arg(long, value_name = "FILE")] - genesis_block: PathBuf, + /// Bootstrap from a trusted genesis block file. + #[arg(long = "file", value_name = "FILE")] + genesis_block_file: Option, + + /// Bootstrap for an official Miden network. + #[arg(long, value_enum, value_name = "NETWORK")] + network: Option, }, } @@ -143,10 +157,15 @@ impl NtxBuilderCommand { pub async fn handle(self) -> anyhow::Result<()> { match self { Self::Start { .. } => self.start().await, - Self::Bootstrap { data_directory, genesis_block } => { + Self::Bootstrap { + data_directory, + genesis_block_file, + network, + } => { ensure_empty_directory(&data_directory)?; let database_filepath = data_directory.join("ntx-builder.sqlite3"); - let genesis = read_genesis_block(&genesis_block)?; + let genesis = + read_bootstrap_genesis_block(genesis_block_file.as_deref(), network).await?; miden_ntx_builder::bootstrap(database_filepath, &genesis) .await .context("failed to bootstrap ntx-builder database") @@ -209,10 +228,15 @@ impl NtxBuilderCommand { } } -/// Reads a genesis block from disk and returns the signed block. -fn read_genesis_block(genesis_block_path: &Path) -> anyhow::Result { - let bytes = fs_err::read(genesis_block_path).context("failed to read genesis block")?; - SignedBlock::read_from_bytes(&bytes).context("failed to deserialize genesis block from file") +async fn read_bootstrap_genesis_block( + genesis_block_file: Option<&Path>, + network: Option, +) -> anyhow::Result { + match (genesis_block_file, network) { + (Some(path), None) => read_signed_genesis_block(path), + (None, Some(network)) => fetch_signed_genesis_block(network).await, + _ => unreachable!("clap requires exactly one genesis block source"), + } } #[cfg(test)] @@ -254,16 +278,22 @@ mod tests { "bootstrap", "--data-directory", "/tmp/miden-ntx-builder", - "--genesis-block", + "--file", "/tmp/genesis.dat", ]) .expect("command should parse"); - let NtxBuilderCommand::Bootstrap { data_directory, genesis_block } = command else { + let NtxBuilderCommand::Bootstrap { + data_directory, + genesis_block_file, + network, + } = command + else { panic!("expected the bootstrap command"); }; assert_eq!(data_directory, PathBuf::from("/tmp/miden-ntx-builder")); - assert_eq!(genesis_block, PathBuf::from("/tmp/genesis.dat")); + assert_eq!(genesis_block_file, Some(PathBuf::from("/tmp/genesis.dat"))); + assert_eq!(network, None); } } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index f3f677018..8364e3633 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -38,6 +38,7 @@ opentelemetry = { workspace = true } opentelemetry-otlp = { default-features = false, features = ["grpc-tonic", "tls-roots", "trace"], version = "0.31" } opentelemetry_sdk = { features = ["rt-tokio", "testing"], version = "0.31" } rand = { workspace = true } +reqwest = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tonic = { default-features = true, workspace = true } diff --git a/crates/utils/src/genesis.rs b/crates/utils/src/genesis.rs new file mode 100644 index 000000000..7fda45e26 --- /dev/null +++ b/crates/utils/src/genesis.rs @@ -0,0 +1,58 @@ +use std::fmt; +use std::path::Path; + +use anyhow::Context; +use miden_protocol::block::SignedBlock; +use miden_protocol::utils::serde::Deserializable; + +/// Official Miden networks with a hosted genesis block. +#[derive(clap::ValueEnum, Clone, Copy, Debug, Eq, PartialEq)] +pub enum OfficialNetwork { + Devnet, + Testnet, +} + +impl OfficialNetwork { + pub const fn as_str(self) -> &'static str { + match self { + Self::Devnet => "devnet", + Self::Testnet => "testnet", + } + } + + pub fn genesis_block_url(self) -> String { + format!("https://genesis.{}.miden.io", self.as_str()) + } +} + +impl fmt::Display for OfficialNetwork { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Reads a trusted, signed genesis block from disk. +pub fn read_signed_genesis_block(path: &Path) -> anyhow::Result { + let bytes = fs_err::read(path).context("failed to read genesis block file")?; + deserialize_signed_genesis_block(&bytes) +} + +/// Downloads a trusted, signed genesis block for an official Miden network. +pub async fn fetch_signed_genesis_block(network: OfficialNetwork) -> anyhow::Result { + let url = network.genesis_block_url(); + let response = reqwest::get(url.as_str()) + .await + .with_context(|| format!("failed to fetch genesis block from {url}"))? + .error_for_status() + .with_context(|| format!("failed to fetch genesis block from {url}"))?; + let bytes = response + .bytes() + .await + .with_context(|| format!("failed to read genesis block response from {url}"))?; + + deserialize_signed_genesis_block(&bytes) +} + +fn deserialize_signed_genesis_block(bytes: &[u8]) -> anyhow::Result { + SignedBlock::read_from_bytes(bytes).context("failed to deserialize genesis block") +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 41e4f9ae6..e4807c506 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -6,6 +6,7 @@ pub mod fee; pub mod fifo_cache; pub mod formatting; pub mod fs; +pub mod genesis; pub mod grpc; pub mod limiter; pub mod logging; diff --git a/docker-compose.yml b/docker-compose.yml index 567343e81..488d33459 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,7 +49,7 @@ services: echo "Bootstrapping node..." miden-node bootstrap \ --data-directory /data/node \ - --genesis-block /data/genesis/genesis.dat + --file /data/genesis/genesis.dat touch /data/node/.bootstrapped @@ -77,7 +77,7 @@ services: echo "Bootstrapping network transaction builder..." miden-ntx-builder bootstrap \ --data-directory /data/ntx-builder \ - --genesis-block /data/genesis/genesis.dat + --file /data/genesis/genesis.dat touch /data/ntx-builder/.bootstrapped diff --git a/docs/external/src/operator/usage.md b/docs/external/src/operator/usage.md index 87d35fede..2167c88ba 100644 --- a/docs/external/src/operator/usage.md +++ b/docs/external/src/operator/usage.md @@ -24,10 +24,10 @@ miden-validator bootstrap \ --genesis-block-directory genesis-data \ --accounts-directory accounts -# Step 2: Store bootstrap — initialize the store database from the genesis block. -miden-node store bootstrap \ +# Step 2: Node bootstrap — initialize the node database from the genesis block. +miden-node bootstrap \ --data-directory store-data \ - --genesis-block genesis-data/genesis.dat + --file genesis-data/genesis.dat ``` You can also configure the account and asset data in the genesis block by passing in a toml configuration file. diff --git a/scripts/run-node.sh b/scripts/run-node.sh index 930debdb3..078e8a2d5 100755 --- a/scripts/run-node.sh +++ b/scripts/run-node.sh @@ -67,7 +67,7 @@ bootstrap_node_data_dir() { echo "Bootstrapping $label..." "$NODE_BINARY" bootstrap \ --data-directory "$data_dir" \ - --genesis-block "$VALIDATOR_DIR/genesis.dat" + --file "$VALIDATOR_DIR/genesis.dat" } bootstrap_ntx_builder() { @@ -75,7 +75,7 @@ bootstrap_ntx_builder() { "$NTX_BUILDER_BINARY" bootstrap \ --data-directory "$NTX_BUILDER_DIR" \ - --genesis-block "$VALIDATOR_DIR/genesis.dat" + --file "$VALIDATOR_DIR/genesis.dat" } node_resource_attributes() {