diff --git a/Cargo.lock b/Cargo.lock index 4b46cac693..9d4bd0bde4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4151,6 +4151,8 @@ dependencies = [ "miden-node-tracing", "miden-node-utils", "miden-protocol", + "miden-standards", + "rand 0.10.2", "serde", "serde_json", "tempfile", diff --git a/bin/node/Cargo.toml b/bin/node/Cargo.toml index 12f6e670c3..1beb5234ab 100644 --- a/bin/node/Cargo.toml +++ b/bin/node/Cargo.toml @@ -30,6 +30,8 @@ miden-node-store = { workspace = true } miden-node-tracing = { workspace = true } miden-node-utils = { workspace = true } miden-protocol = { workspace = true } +miden-standards = { workspace = true } +rand = { workspace = true } serde = { features = ["derive"], workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/bin/node/src/commands/lifecycle.rs b/bin/node/src/commands/lifecycle.rs index 648e12c195..29beab4ff1 100644 --- a/bin/node/src/commands/lifecycle.rs +++ b/bin/node/src/commands/lifecycle.rs @@ -1,3 +1,4 @@ +use std::io::Write; use std::path::{Path, PathBuf}; use anyhow::Context; @@ -8,9 +9,48 @@ use miden_node_store::{DataDirectory, Db, State}; use miden_node_tracing::info; use miden_node_utils::fs::ensure_empty_directory; use miden_node_utils::genesis::{OfficialNetwork, fetch_genesis_block, read_genesis_block}; +use miden_protocol::account::auth::AuthSecretKey; +use miden_protocol::account::{AccountBuilder, AccountFile, AccountType}; +use miden_protocol::utils::serde::Serializable; +use miden_standards::account::auth::AuthTxFeeCollector; +use miden_standards::account::wallets::BasicWallet; use super::ENV_DATA_DIRECTORY; +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bootstrap_saves_a_new_collector_and_preserves_its_signing_key() -> anyhow::Result<()> { + let directory = tempfile::tempdir()?; + let directory = DataDirectory::load(directory.path().to_path_buf())?; + create_collection_account(&directory)?; + let path = directory.batch_builder_collection_account_path(); + let account_file = AccountFile::read(&path)?; + assert!(account_file.account.is_new()); + assert!(account_file.account.is_public()); + assert!(account_file.account.vault().is_empty()); + assert_eq!(account_file.auth_secret_keys.len(), 1); + assert_eq!( + account_file.account.storage().get_item(AuthTxFeeCollector::public_key_slot())?, + miden_protocol::Word::from( + account_file.auth_secret_keys[0].public_key().to_commitment() + ), + ); + + let contents = fs_err::read(&path)?; + assert!(create_collection_account(&directory).is_err()); + assert_eq!(fs_err::read(&path)?, contents); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!(fs_err::metadata(&path)?.permissions().mode() & 0o777, 0o600); + } + Ok(()) + } +} + // BOOTSTRAP // ================================================================================================ @@ -58,6 +98,8 @@ impl BootstrapCommand { read_bootstrap_genesis_block(self.genesis_block_file.as_deref(), self.network).await?; let genesis_commitment = genesis_block.inner().header().commitment(); State::bootstrap(genesis_block, &self.data_directory)?; + create_collection_account(&DataDirectory::load(self.data_directory.clone())?) + .context("failed to create the batch builder collection account")?; info!( target: crate::LOG_TARGET, "Node bootstrap complete", @@ -68,6 +110,28 @@ impl BootstrapCommand { } } +/// Saves the collector and its signing key without registering the account on-chain. +fn create_collection_account(directory: &DataDirectory) -> anyhow::Result<()> { + let secret_key = AuthSecretKey::new_falcon512_poseidon2(); + let account = AccountBuilder::new(rand::random()) + .account_type(AccountType::Public) + .with_component(AuthTxFeeCollector::from_public_key(secret_key.public_key())) + .with_component(BasicWallet) + .build()?; + let account_file = AccountFile::new(account, vec![secret_key]); + let mut options = fs_err::OpenOptions::new(); + options.create_new(true).write(true); + #[cfg(unix)] + { + use fs_err::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options.open(directory.batch_builder_collection_account_path())?; + file.write_all(&account_file.to_bytes())?; + file.sync_all()?; + Ok(()) +} + /// Reads the genesis block from the configured source and validates it. async fn read_bootstrap_genesis_block( genesis_block_file: Option<&Path>, diff --git a/crates/store/src/data_directory.rs b/crates/store/src/data_directory.rs index 975dbeabb2..420e156bfb 100644 --- a/crates/store/src/data_directory.rs +++ b/crates/store/src/data_directory.rs @@ -33,6 +33,10 @@ impl DataDirectory { self.0.join("miden-allowlist.sqlite3") } + pub fn batch_builder_collection_account_path(&self) -> PathBuf { + self.0.join("batch_builder_collection_account.mac") + } + pub fn display(&self) -> std::path::Display<'_> { self.0.display() } diff --git a/docs/external/src/network-operator/bootstrap-and-genesis.md b/docs/external/src/network-operator/bootstrap-and-genesis.md index e0fd9f715f..c2433907ab 100644 --- a/docs/external/src/network-operator/bootstrap-and-genesis.md +++ b/docs/external/src/network-operator/bootstrap-and-genesis.md @@ -25,6 +25,9 @@ which provides an easy method to obtain this data. This is directly supported by `--network testnet` or `--network devnet`. Bootstrap commands also support passing a file directly to cover custom networks, or if the official URLs are not trusted. +Node bootstrap also creates `batch_builder_collection_account.mac` in the node data directory. Keep this file private +because it contains the collection account's signing key. + ## Bootstrap Flow