Skip to content
Open
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
84 changes: 55 additions & 29 deletions bin/node/src/commands/modes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl SequencerCommand {

let sequencer = Sequencer {
store: Arc::clone(&state),
validator_url: self.external_services.validator_url.clone(),
validator_urls: self.external_services.validator_urls.clone(),
validator_timeout: self.external_services.validator_timeout,
batch_prover_url: self.block_producer.batch.prover_url,
block_prover_url: self.block_producer.block_prover.url,
Expand All @@ -82,7 +82,7 @@ impl SequencerCommand {
store: state,
mode: RpcMode::sequencer(
block_producer.clone(),
self.external_services.validator_client()?,
self.external_services.validator_clients()?,
),
ntx_builder: Some(self.external_services.ntx_builder_client()?),
grpc_options: runtime.external_grpc_options,
Expand All @@ -106,9 +106,18 @@ impl SequencerCommand {

#[derive(clap::Args, Clone, Debug)]
pub struct SequencerExternalServiceOptions {
/// The validator service gRPC URL.
#[arg(long = "validator.url", env = "MIDEN_NODE_VALIDATOR_URL", value_name = "URL")]
pub validator_url: Url,
/// The validator service gRPC URLs.
///
/// One URL per validator; repeat the argument or comma-separate the values. Transactions are
/// submitted to, and blocks are signed by, every validator.
#[arg(
long = "validator.url",
env = "MIDEN_NODE_VALIDATOR_URL",
value_name = "URL",
value_delimiter = ',',
required = true
)]
pub validator_urls: Vec<Url>,

/// Request timeout for calls to the validator service.
///
Expand All @@ -129,14 +138,19 @@ pub struct SequencerExternalServiceOptions {
}

impl SequencerExternalServiceOptions {
fn validator_client(&self) -> anyhow::Result<ValidatorClient> {
Ok(Builder::new(self.validator_url.clone())
.with_tls()?
.with_timeout(self.validator_timeout)
.without_metadata_version()
.without_metadata_genesis()
.with_otel_context_injection()
.connect_lazy::<ValidatorClient>())
fn validator_clients(&self) -> anyhow::Result<Vec<ValidatorClient>> {
self.validator_urls
.iter()
.map(|url| {
Ok(Builder::new(url.clone())
.with_tls()?
.with_timeout(self.validator_timeout)
.without_metadata_version()
.without_metadata_genesis()
.with_otel_context_injection()
.connect_lazy::<ValidatorClient>())
})
.collect()
}

fn ntx_builder_client(&self) -> anyhow::Result<NtxBuilderClient> {
Expand All @@ -161,21 +175,25 @@ pub struct FullNodeCommand {
#[command(flatten)]
pub store: StoreOptions,

/// The validator service gRPC URL.
/// The validator service gRPC URLs.
///
/// One URL per validator; repeat the argument or comma-separate the values. Transactions are
/// submitted to every validator.
#[arg(
long = "validator.url",
env = "MIDEN_NODE_VALIDATOR_URL",
value_name = "URL",
value_delimiter = ',',
requires = "sequencer_url"
)]
pub validator_url: Option<Url>,
pub validator_urls: Vec<Url>,

/// The sequencer's internal service gRPC URL.
#[arg(
long = "sequencer.internal.url",
env = "MIDEN_NODE_SEQUENCER_INTERNAL_URL",
value_name = "URL",
requires = "validator_url"
requires = "validator_urls"
)]
pub sequencer_url: Option<Url>,
}
Expand All @@ -184,7 +202,7 @@ impl FullNodeCommand {
pub async fn handle(self, shutdown: CancellationToken) -> anyhow::Result<()> {
let runtime = self.runtime.runtime_config(&self.store);
let source_rpc = self.sync.source_rpc_client()?;
let validator_client = self.validator_client();
let validator_clients = self.validator_clients();
let sequencer_client = self.sequencer_client();
let network_tx_auth = self.runtime.rpc.network_tx_auth()?;
let state = load_state(&runtime).await?;
Expand All @@ -196,7 +214,7 @@ impl FullNodeCommand {
mode: RpcMode::full_node(
source_rpc,
self.sync.readiness_threshold,
validator_client,
validator_clients,
sequencer_client,
),
ntx_builder: None,
Expand All @@ -222,17 +240,25 @@ impl FullNodeCommand {
})
}

fn validator_client(&self) -> Option<ValidatorClient> {
self.validator_url.as_ref().map(|url| {
Builder::new(url.clone())
.with_tls()
.expect("TLS is enabled")
.with_timeout(Duration::from_secs(5))
.without_metadata_version()
.without_metadata_genesis()
.with_otel_context_injection()
.connect_lazy::<ValidatorClient>()
})
fn validator_clients(&self) -> Option<Vec<ValidatorClient>> {
if self.validator_urls.is_empty() {
return None;
}
let clients = self
.validator_urls
.iter()
.map(|url| {
Builder::new(url.clone())
.with_tls()
.expect("TLS is enabled")
.with_timeout(Duration::from_secs(5))
.without_metadata_version()
.without_metadata_genesis()
.with_otel_context_injection()
.connect_lazy::<ValidatorClient>()
})
.collect();
Some(clients)
}
}

Expand Down
4 changes: 1 addition & 3 deletions bin/ntx-builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ fn validate_genesis_block(block: &SignedBlock) -> anyhow::Result<()> {
block.header().block_num(),
);

block
.signatures()
.verify_against(block.header().commitment(), block.header().validator_keys())
miden_node_utils::genesis::verify_genesis_signatures(block.header(), block.signatures())
.context("genesis block signature verification failed")?;

Ok(())
Expand Down
9 changes: 8 additions & 1 deletion bin/stress-test/src/seeding/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use miden_protocol::block::{
FeeParameters,
ProposedBlock,
SignedBlock,
ValidatorKeys,
};
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey as EcdsaSecretKey;
use miden_protocol::crypto::dsa::falcon512_poseidon2::{PublicKey, SecretKey};
Expand Down Expand Up @@ -114,7 +115,13 @@ pub async fn seed_store(
let asset_faucet_ids = benchmark_faucets.iter().map(Account::id).collect::<Vec<_>>();
let fee_params = FeeParameters::new(faucet.id(), 0);
let signer = EcdsaSecretKey::new();
let genesis_state = GenesisState::new(benchmark_faucets, fee_params, 1, 1, signer.public_key());
let genesis_state = GenesisState::new(
benchmark_faucets,
fee_params,
1,
1,
ValidatorKeys::new(vec![signer.public_key()]).unwrap(),
);
let genesis_block = genesis_state
.clone()
.into_block(&signer)
Expand Down
111 changes: 83 additions & 28 deletions bin/validator/src/commands/bootstrap.rs
Comment thread
Mirko-von-Leipzig marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,40 @@ use std::path::{Path, PathBuf};

use anyhow::Context;
use miden_node_store::BlockStore;
use miden_node_store::genesis::GenesisBlock;
use miden_node_store::genesis::config::{AccountFileWithName, GenesisConfig};
use miden_node_utils::fs::ensure_empty_directory;
use miden_node_utils::genesis::read_signed_genesis_block;
use miden_protocol::block::BlockSignatures;
use miden_protocol::utils::serde::Serializable;
use miden_validator::{DataDirectory, ValidatorSigner};
use miden_validator::DataDirectory;

use super::ValidatorKey;
use super::ValidatorKeyArgs;

// Bootstraps the validator component.
pub async fn bootstrap(
/// Runs the signing form of `bootstrap`: builds and signs the genesis block with this
/// validator's key.
///
/// Builds the genesis state from the given configuration — or from [`GenesisConfig::default`]
/// when `None`, as in local development where the built-in single-validator configuration
/// suffices — writes the account secret files, signs the genesis block with this validator's
/// key, and persists the block as the chain tip.
///
/// The genesis header commits to the full validator set, taken from the `validators` public
/// keys in the genesis configuration (defaulting to this validator's key alone). Only this
/// validator signs the genesis block; the full set is required to sign from the next block
/// onwards, so bootstrapping does not need signing access to the other validators' keys.
///
/// Every other validator seeds from this form's output via [`bootstrap_from_file`].
pub async fn bootstrap_sign(
genesis_block_directory: &Path,
accounts_directory: &Path,
data_directory: &Path,
sqlite_connection_pool_size: NonZeroUsize,
genesis_config: Option<&PathBuf>,
validator_key: ValidatorKey,
validator_keys: ValidatorKeyArgs,
) -> anyhow::Result<()> {
let dirs = load_bootstrap_dirs(genesis_block_directory, accounts_directory, data_directory)?;

let config = genesis_config
.map(|file_path| {
GenesisConfig::read_toml_file(file_path).with_context(|| {
Expand All @@ -28,28 +46,7 @@ pub async fn bootstrap(
.transpose()?
.unwrap_or_default();

for directory in [accounts_directory, genesis_block_directory, data_directory] {
ensure_empty_directory(directory)?;
}

let signer = validator_key.into_signer().await?;
let dirs = DataDirectory::load_bootstrap(
genesis_block_directory.to_path_buf(),
accounts_directory.to_path_buf(),
data_directory.to_path_buf(),
)
.context("failed to load bootstrap directories")?;
build_and_write_genesis(config, signer, dirs, sqlite_connection_pool_size).await
}

/// Builds the genesis state, writes account secret files, signs the genesis block, writes it to
/// disk, and initializes the validator's database with the genesis block as the chain tip.
async fn build_and_write_genesis(
config: GenesisConfig,
signer: ValidatorSigner,
dirs: DataDirectory,
sqlite_connection_pool_size: NonZeroUsize,
) -> anyhow::Result<()> {
let signer = validator_keys.into_signer().await?;
let (genesis_state, secrets) = config.into_state(signer.public_key())?;

for item in secrets.as_account_files(&genesis_state) {
Expand All @@ -67,14 +64,72 @@ async fn build_and_write_genesis(
let unsigned_genesis_block = genesis_state
.into_unsigned_block()
.context("failed to build the unsigned genesis block")?;

// Sign the genesis block with this validator's key only. The other validators' keys are
// committed to by the genesis header and must sign from the next block onwards.
let signature = signer
.sign(unsigned_genesis_block.header())
.await
.context("failed to sign the genesis block")?;
let signatures = BlockSignatures::new(vec![signature])
.context("failed to build the genesis block signatures")?;

let genesis_block = unsigned_genesis_block
.into_block(signature)
.into_block(signatures)
.context("failed to build the genesis block")?;

persist_genesis(genesis_block, dirs, sqlite_connection_pool_size).await
}

/// Runs the seeding form of `bootstrap`: initializes this validator from the genesis block
/// produced by the signing form ([`bootstrap_sign`]), without re-signing it.
///
/// The genesis block is the chain's trust root and carries a single signature from the
/// bootstrapping validator, verified against the validator set committed to by its header
/// (via [`GenesisBlock::try_from`]). This form verifies the block and persists it as the
/// chain tip.
pub async fn bootstrap_from_file(
genesis_block_directory: &Path,
accounts_directory: &Path,
data_directory: &Path,
sqlite_connection_pool_size: NonZeroUsize,
genesis_block_file: &Path,
) -> anyhow::Result<()> {
let dirs = load_bootstrap_dirs(genesis_block_directory, accounts_directory, data_directory)?;

let signed_block = read_signed_genesis_block(genesis_block_file)
.context("failed to read genesis block file")?;
let genesis_block =
GenesisBlock::try_from(signed_block).context("genesis block validation failed")?;

persist_genesis(genesis_block, dirs, sqlite_connection_pool_size).await
}

/// Empties the bootstrap directories and loads them as a [`DataDirectory`].
fn load_bootstrap_dirs(
genesis_block_directory: &Path,
accounts_directory: &Path,
data_directory: &Path,
) -> anyhow::Result<DataDirectory> {
for directory in [accounts_directory, genesis_block_directory, data_directory] {
ensure_empty_directory(directory)?;
}

DataDirectory::load_bootstrap(
genesis_block_directory.to_path_buf(),
accounts_directory.to_path_buf(),
data_directory.to_path_buf(),
)
.context("failed to load bootstrap directories")
}

/// Writes the genesis block file, bootstraps the block store, and initializes the validator's
/// database with the genesis block as the chain tip.
async fn persist_genesis(
genesis_block: GenesisBlock,
dirs: DataDirectory,
sqlite_connection_pool_size: NonZeroUsize,
) -> anyhow::Result<()> {
let block_bytes = genesis_block.inner().to_bytes();
fs_err::write(dirs.genesis_block_path().expect("bootstrap directories"), block_bytes)
.context("failed to write genesis block")?;
Expand Down
Loading
Loading