From 9504dc5a86432526a4025538b2cf6251ee20c241 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 11 Feb 2025 14:31:33 -0500 Subject: [PATCH] katana init output path --- bin/katana/src/cli/config.rs | 7 +- bin/katana/src/cli/init/mod.rs | 18 ++- bin/katana/src/cli/mod.rs | 2 +- crates/katana/chain-spec/src/rollup/file.rs | 151 ++++++++++++++++---- crates/katana/chain-spec/src/rollup/mod.rs | 3 +- crates/katana/cli/src/args.rs | 14 +- crates/katana/cli/src/utils.rs | 24 +++- 7 files changed, 170 insertions(+), 49 deletions(-) diff --git a/bin/katana/src/cli/config.rs b/bin/katana/src/cli/config.rs index 7ffebd4435..3331187938 100644 --- a/bin/katana/src/cli/config.rs +++ b/bin/katana/src/cli/config.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Args; -use katana_chain_spec::rollup::file::ChainConfigDir; +use katana_chain_spec::rollup::LocalChainConfigDir; use katana_primitives::chain::ChainId; use starknet::core::utils::parse_cairo_short_string; @@ -15,14 +15,13 @@ impl ConfigArgs { pub fn execute(self) -> Result<()> { match self.chain { Some(chain) => { - let cs = ChainConfigDir::open(&chain)?; - let path = cs.config_path(); + let path = LocalChainConfigDir::open(&chain)?.config_path(); let config = std::fs::read_to_string(&path)?; println!("File: {}\n\n{config}", path.display()); } None => { - let chains = katana_chain_spec::rollup::file::list()?; + let chains = katana_chain_spec::rollup::list()?; for chain in chains { // TODO: // We can't just assume that the id is a valid (and readable) ascii string diff --git a/bin/katana/src/cli/init/mod.rs b/bin/katana/src/cli/init/mod.rs index 2fe0c2011c..f83145c0b0 100644 --- a/bin/katana/src/cli/init/mod.rs +++ b/bin/katana/src/cli/init/mod.rs @@ -1,9 +1,10 @@ +use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use anyhow::Context; use clap::Args; -use katana_chain_spec::rollup::FeeContract; +use katana_chain_spec::rollup::{ChainConfigDir, FeeContract}; use katana_chain_spec::{rollup, SettlementLayer}; use katana_primitives::chain::ChainId; use katana_primitives::genesis::allocation::DevAllocationsGenerator; @@ -43,6 +44,10 @@ pub struct InitArgs { #[arg(long = "settlement-contract")] #[arg(requires_all = ["id", "settlement_chain", "settlement_account", "settlement_account_private_key"])] settlement_contract: Option, + + /// Specify the path of the directory where the configuration files will be stored at. + #[arg(long)] + output_path: Option, } impl InitArgs { @@ -66,9 +71,16 @@ impl InitArgs { let genesis = GENESIS.clone(); // At the moment, the fee token is limited to a predefined token. let fee_contract = FeeContract::default(); - let chain_spec = rollup::ChainSpec { id, genesis, settlement, fee_contract }; - rollup::file::write(&chain_spec).context("failed to write chain spec file")?; + + if let Some(path) = self.output_path { + let dir = ChainConfigDir::create(path)?; + rollup::write(&dir, &chain_spec).context("failed to write chain spec file")?; + } else { + // Write to the local chain config directory by default if user + // doesn't specify the output path + rollup::write_local(&chain_spec).context("failed to write chain spec file")?; + } Ok(()) } diff --git a/bin/katana/src/cli/mod.rs b/bin/katana/src/cli/mod.rs index 8f5fbfb483..1fc90ae713 100644 --- a/bin/katana/src/cli/mod.rs +++ b/bin/katana/src/cli/mod.rs @@ -39,7 +39,7 @@ impl Cli { #[derive(Subcommand)] enum Commands { #[command(about = "Initialize chain")] - Init(init::InitArgs), + Init(Box), #[command(about = "Chain configuration utilities")] Config(config::ConfigArgs), diff --git a/crates/katana/chain-spec/src/rollup/file.rs b/crates/katana/chain-spec/src/rollup/file.rs index 12529bfdd7..88f2cd969b 100644 --- a/crates/katana/chain-spec/src/rollup/file.rs +++ b/crates/katana/chain-spec/src/rollup/file.rs @@ -1,4 +1,4 @@ -use std::fs::File; +use std::fs::{self, File}; use std::io::{self, BufReader, BufWriter}; use std::path::{Path, PathBuf}; @@ -16,8 +16,11 @@ pub enum Error { #[error("OS not supported")] UnsupportedOS, - #[error("config directory not found for chain `{id}`")] - DirectoryNotFound { id: String }, + #[error("no local config directory found for chain `{id}`")] + LocalConfigDirectoryNotFound { id: String }, + + #[error("chain config path must be a directory")] + MustBeADirectory, #[error("failed to read config file: {0}")] ConfigReadError(#[from] toml::ser::Error), @@ -32,12 +35,14 @@ pub enum Error { GenesisJson(#[from] katana_primitives::genesis::json::GenesisJsonError), } -pub fn read(id: &ChainId) -> Result { - read_at(local_dir()?, id) +/// Read the [`ChainSpec`] of the given `id` from the local config directory. +pub fn read_local(id: &ChainId) -> Result { + read(&ChainConfigDir::open_local(id)?) } -pub fn write(chain_spec: &ChainSpec) -> Result<(), Error> { - write_at(local_dir()?, chain_spec) +/// Write the given [`ChainSpec`] at the local config directory based on it's id. +pub fn write_local(chain_spec: &ChainSpec) -> Result<(), Error> { + write(&ChainConfigDir::create_local(&chain_spec.id)?, chain_spec) } /// List all of the available chain configurations. @@ -48,9 +53,7 @@ pub fn list() -> Result, Error> { list_at(local_dir()?) } -fn read_at>(dir: P, id: &ChainId) -> Result { - let dir = ChainConfigDir::open_at(dir, id)?; - +pub fn read(dir: &ChainConfigDir) -> Result { let chain_spec: ChainSpecFile = { let content = std::fs::read_to_string(dir.config_path())?; toml::from_str(&content)? @@ -70,9 +73,7 @@ fn read_at>(dir: P, id: &ChainId) -> Result { }) } -fn write_at>(dir: P, chain_spec: &ChainSpec) -> Result<(), Error> { - let dir = ChainConfigDir::create_at(dir, &chain_spec.id)?; - +pub fn write(dir: &ChainConfigDir, chain_spec: &ChainSpec) -> Result<(), Error> { { let cfg = ChainSpecFile { id: chain_spec.id, @@ -109,7 +110,7 @@ fn list_at>(dir: P) -> Result, Error> { if entry.file_type()?.is_dir() { if let Some(name) = entry.file_name().to_str() { if let Ok(chain_id) = ChainId::parse(name) { - let cs = ChainConfigDir::open_at(dir, &chain_id).expect("must exist"); + let cs = LocalChainConfigDir::open_at(dir, &chain_id).expect("must exist"); if cs.config_path().exists() { chains.push(chain_id); } @@ -133,11 +134,61 @@ struct ChainSpecFile { /// The local directory name where the chain configuration files are stored. const KATANA_LOCAL_DIR: &str = "katana"; -// > LOCAL_DIR/$chain_id/ -#[derive(Debug, Clone)] -pub struct ChainConfigDir(PathBuf); +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ChainConfigDir { + Absolute(PathBuf), + Local(LocalChainConfigDir), +} impl ChainConfigDir { + pub fn create_local(id: &ChainId) -> Result { + Ok(Self::Local(LocalChainConfigDir::create(id)?)) + } + + pub fn open_local(id: &ChainId) -> Result { + Ok(Self::Local(LocalChainConfigDir::open(id)?)) + } + + pub fn create>(path: P) -> Result { + let path = path.as_ref(); + + if !path.exists() { + std::fs::create_dir_all(path)?; + } + + Ok(ChainConfigDir::Absolute(path.to_path_buf())) + } + + pub fn open>(path: P) -> Result { + let path = fs::canonicalize(path)?; + + if !path.is_dir() { + return Err(Error::MustBeADirectory); + } + + Ok(Self::Absolute(path.to_path_buf())) + } + + pub fn config_path(&self) -> PathBuf { + match self { + Self::Absolute(path) => path.join("config").with_extension("toml"), + Self::Local(local) => local.config_path(), + } + } + + pub fn genesis_path(&self) -> PathBuf { + match self { + Self::Absolute(path) => path.join("genesis").with_extension("json"), + Self::Local(local) => local.genesis_path(), + } + } +} + +// > LOCAL_DIR/$chain_id/ +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LocalChainConfigDir(PathBuf); + +impl LocalChainConfigDir { /// Creates a new config directory for the given chain ID. /// /// The directory will be created at `$LOCAL_DIR/`, where `$LOCAL_DIR` is the path returned @@ -160,9 +211,10 @@ impl ChainConfigDir { Self::open_at(local_dir()?, id) } - pub fn create_at>(dir: P, id: &ChainId) -> Result { + /// Same like [`Self::create`] but at a specific base path instead of `$LOCAL_DIR`. + pub fn create_at>(base: P, id: &ChainId) -> Result { let id = id.to_string(); - let path = dir.as_ref().join(id); + let path = base.as_ref().join(id); if !path.exists() { std::fs::create_dir_all(&path)?; @@ -171,12 +223,13 @@ impl ChainConfigDir { Ok(Self(path)) } - pub fn open_at>(dir: P, id: &ChainId) -> Result { + /// Same like [`Self::open`] but at a specific base path instead of `$LOCAL_DIR`. + pub fn open_at>(base: P, id: &ChainId) -> Result { let id = id.to_string(); - let path = dir.as_ref().join(&id); + let path = base.as_ref().join(&id); if !path.exists() { - return Err(Error::DirectoryNotFound { id: id.clone() }); + return Err(Error::LocalConfigDirectoryNotFound { id: id.clone() }); } Ok(Self(path)) @@ -212,6 +265,7 @@ pub fn local_dir() -> Result { #[cfg(test)] mod tests { + use std::fs; use std::path::Path; use std::sync::OnceLock; @@ -222,7 +276,7 @@ mod tests { use url::Url; use super::Error; - use crate::rollup::file::{local_dir, ChainConfigDir, KATANA_LOCAL_DIR}; + use crate::rollup::file::{local_dir, ChainConfigDir, LocalChainConfigDir, KATANA_LOCAL_DIR}; use crate::rollup::{ChainSpec, FeeContract}; use crate::SettlementLayer; @@ -234,15 +288,21 @@ mod tests { /// Test version of [`super::read`]. fn read(id: &ChainId) -> Result { - with_temp_dir(|dir| super::read_at(dir, id)) + with_temp_dir(|dir| { + let dir = LocalChainConfigDir::open_at(dir, id)?; + super::read(&ChainConfigDir::Local(dir)) + }) } /// Test version of [`super::write`]. fn write(chain_spec: &ChainSpec) -> Result<(), Error> { - with_temp_dir(|dir| super::write_at(dir, chain_spec)) + with_temp_dir(|dir| { + let dir = LocalChainConfigDir::create_at(dir, &chain_spec.id)?; + super::write(&ChainConfigDir::Local(dir), chain_spec) + }) } - impl ChainConfigDir { + impl LocalChainConfigDir { fn open_tmp(id: &ChainId) -> Result { with_temp_dir(|dir| Self::open_at(dir, id)) } @@ -284,16 +344,19 @@ mod tests { let chain_id = ChainId::parse("test").unwrap(); // Test creation - let config_dir = ChainConfigDir::create_tmp(&chain_id).unwrap(); + let config_dir = LocalChainConfigDir::create_tmp(&chain_id).unwrap(); assert!(config_dir.0.exists()); // Test opening existing dir - let opened_dir = ChainConfigDir::open_tmp(&chain_id).unwrap(); + let opened_dir = LocalChainConfigDir::open_tmp(&chain_id).unwrap(); assert_eq!(config_dir.0, opened_dir.0); // Test opening non-existent dir let bad_id = ChainId::parse("nonexistent").unwrap(); - assert!(matches!(ChainConfigDir::open_tmp(&bad_id), Err(Error::DirectoryNotFound { .. }))); + assert!(matches!( + LocalChainConfigDir::open_tmp(&bad_id), + Err(Error::LocalConfigDirectoryNotFound { .. }) + )); } #[test] @@ -305,7 +368,7 @@ mod tests { #[test] fn test_config_paths() { let chain_id = ChainId::parse("test").unwrap(); - let config_dir = ChainConfigDir::create_tmp(&chain_id).unwrap(); + let config_dir = LocalChainConfigDir::create_tmp(&chain_id).unwrap(); assert!(config_dir.config_path().ends_with("config.toml")); assert!(config_dir.genesis_path().ends_with("genesis.json")); @@ -329,10 +392,36 @@ mod tests { // Write them to disk for spec in &chain_specs { - super::write_at(&dir, spec).unwrap(); + let id = &spec.id; + let dir = LocalChainConfigDir::create_at(&dir, id).unwrap(); + super::write(&ChainConfigDir::Local(dir), spec).unwrap(); } let listed_chains = super::list_at(&dir).unwrap(); assert_eq!(listed_chains.len(), chain_specs.len()); } + + #[test] + fn test_absolute_chain_config_dir() { + let temp_dir = tempfile::tempdir().unwrap(); + let path = temp_dir.path(); + + // Test creating absolute dir + let chain_dir = ChainConfigDir::create(path).unwrap(); + match &chain_dir { + ChainConfigDir::Absolute(p) => assert_eq!(p, &path), + _ => panic!("Expected Absolute variant"), + } + + // Test opening existing absolute dir + let opened_dir = ChainConfigDir::open(path).unwrap(); + match opened_dir { + ChainConfigDir::Absolute(p) => assert_eq!(p, fs::canonicalize(path).unwrap()), + _ => panic!("Expected Absolute variant"), + } + + // Test error on non-existent dir + let bad_path = path.join("nonexistent"); + assert!(matches!(ChainConfigDir::open(&bad_path), Err(Error::IO(..)))); + } } diff --git a/crates/katana/chain-spec/src/rollup/mod.rs b/crates/katana/chain-spec/src/rollup/mod.rs index 53b58a3be5..15218da79b 100644 --- a/crates/katana/chain-spec/src/rollup/mod.rs +++ b/crates/katana/chain-spec/src/rollup/mod.rs @@ -6,9 +6,10 @@ use katana_primitives::genesis::Genesis; use katana_primitives::version::CURRENT_STARKNET_VERSION; use serde::{Deserialize, Serialize}; -pub mod file; +mod file; mod utils; +pub use file::*; pub use utils::DEFAULT_APPCHAIN_FEE_TOKEN_ADDRESS; use crate::SettlementLayer; diff --git a/crates/katana/cli/src/args.rs b/crates/katana/cli/src/args.rs index abc0025e85..b2034f2c55 100644 --- a/crates/katana/cli/src/args.rs +++ b/crates/katana/cli/src/args.rs @@ -8,6 +8,7 @@ use alloy_primitives::U256; use anyhow::bail; use anyhow::{Context, Result}; use clap::Parser; +use katana_chain_spec::rollup::ChainConfigDir; use katana_chain_spec::ChainSpec; use katana_core::constants::DEFAULT_SEQUENCER_ADDRESS; use katana_core::service::messaging::MessagingConfig; @@ -21,7 +22,6 @@ use katana_node::config::rpc::RpcConfig; use katana_node::config::rpc::{RpcModuleKind, RpcModulesList}; use katana_node::config::sequencing::SequencingConfig; use katana_node::config::Config; -use katana_primitives::chain::ChainId; use katana_primitives::genesis::allocation::DevAllocationsGenerator; use katana_primitives::genesis::constant::DEFAULT_PREFUNDED_ACCOUNT_BALANCE; use serde::{Deserialize, Serialize}; @@ -32,8 +32,7 @@ use url::Url; use crate::file::NodeArgsConfig; use crate::options::*; -use crate::utils; -use crate::utils::{parse_seed, LogFormat}; +use crate::utils::{self, parse_chain_config_dir, parse_seed, LogFormat}; pub(crate) const LOG_TARGET: &str = "katana::cli"; @@ -46,8 +45,8 @@ pub struct NodeArgs { /// Path to the chain configuration file. #[arg(long, hide = true)] - #[arg(value_parser = ChainId::parse)] - pub chain: Option, + #[arg(value_parser = parse_chain_config_dir)] + pub chain: Option, /// Disable auto and interval mining, and mine on demand instead via an endpoint. #[arg(long)] @@ -244,9 +243,8 @@ impl NodeArgs { } fn chain_spec(&self) -> Result> { - if let Some(id) = &self.chain { - let mut cs = - katana_chain_spec::rollup::file::read(id).context("failed to load chain spec")?; + if let Some(path) = &self.chain { + let mut cs = katana_chain_spec::rollup::read(path)?; cs.genesis.sequencer_address = *DEFAULT_SEQUENCER_ADDRESS; Ok(Arc::new(ChainSpec::Rollup(cs))) } diff --git a/crates/katana/cli/src/utils.rs b/crates/katana/cli/src/utils.rs index a94d32578e..1ea018a500 100644 --- a/crates/katana/cli/src/utils.rs +++ b/crates/katana/cli/src/utils.rs @@ -1,12 +1,14 @@ use std::fmt::Display; use std::path::PathBuf; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use clap::builder::PossibleValue; use clap::ValueEnum; use console::Style; +use katana_chain_spec::rollup::ChainConfigDir; use katana_chain_spec::ChainSpec; use katana_primitives::block::{BlockHash, BlockHashOrNumber, BlockNumber}; +use katana_primitives::chain::ChainId; use katana_primitives::class::ClassHash; use katana_primitives::contract::ContractAddress; use katana_primitives::genesis::allocation::GenesisAccountAlloc; @@ -236,6 +238,26 @@ where .map_err(serde::de::Error::custom) } +// Chain IDs can be arbitrary ASCII strings, making them indistinguishable from filesystem paths. +// To handle this ambiguity, we first try parsing single-component inputs as paths, then as chain IDs. +// Multi-component inputs are always treated as paths. +pub fn parse_chain_config_dir(value: &str) -> Result { + let path = PathBuf::from(value); + + if path.components().count() == 1 { + if path.exists() { + Ok(ChainConfigDir::open(path)?) + } else if let Ok(id) = ChainId::parse(value) { + Ok(ChainConfigDir::open_local(&id)?) + } else { + Err(anyhow!("Invalid path or chain id")) + } + } else { + let path = PathBuf::from(shellexpand::tilde(value).as_ref()); + Ok(ChainConfigDir::open(path)?) + } +} + #[cfg(test)] mod tests { use super::*;