From 01111f454a5c07f4b32f4d415b90df27042a5430 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Thu, 23 Jan 2025 13:30:43 -0500 Subject: [PATCH] feat(katana): chain spec file management tools (#2945) --- Cargo.lock | 42 ++++- bin/katana/Cargo.toml | 1 - bin/katana/src/cli/init/mod.rs | 66 +------- crates/katana/chain-spec/Cargo.toml | 3 + crates/katana/chain-spec/src/file.rs | 224 +++++++++++++++++++++++++++ crates/katana/chain-spec/src/lib.rs | 66 +------- crates/katana/cli/src/args.rs | 11 +- 7 files changed, 283 insertions(+), 130 deletions(-) create mode 100644 crates/katana/chain-spec/src/file.rs diff --git a/Cargo.lock b/Cargo.lock index d54d36c734..8ad49068fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4736,6 +4736,15 @@ dependencies = [ "dirs-sys 0.4.1", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -4753,7 +4762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -4765,10 +4774,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.0", + "windows-sys 0.59.0", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -4776,7 +4797,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -8440,7 +8461,6 @@ dependencies = [ "clap", "clap_complete", "comfy-table", - "dirs 5.0.1", "dojo-utils", "inquire", "katana-chain-spec", @@ -8482,6 +8502,7 @@ version = "1.0.12" dependencies = [ "alloy-primitives", "anyhow", + "dirs 6.0.0", "katana-primitives", "lazy_static", "serde", @@ -8489,6 +8510,8 @@ dependencies = [ "similar-asserts", "starknet 0.12.0", "tempfile", + "thiserror 1.0.63", + "toml 0.8.19", "url", ] @@ -12209,6 +12232,17 @@ dependencies = [ "thiserror 1.0.63", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror 2.0.11", +] + [[package]] name = "regex" version = "1.10.6" diff --git a/bin/katana/Cargo.toml b/bin/katana/Cargo.toml index 4f8fc9bd5d..654221323d 100644 --- a/bin/katana/Cargo.toml +++ b/bin/katana/Cargo.toml @@ -21,7 +21,6 @@ cainome.workspace = true clap.workspace = true clap_complete.workspace = true comfy-table = "7.1.1" -dirs = "5.0.1" dojo-utils.workspace = true inquire = "0.7.5" lazy_static.workspace = true diff --git a/bin/katana/src/cli/init/mod.rs b/bin/katana/src/cli/init/mod.rs index a5f57a3de9..aada4f8943 100644 --- a/bin/katana/src/cli/init/mod.rs +++ b/bin/katana/src/cli/init/mod.rs @@ -1,18 +1,15 @@ mod deployment; -use std::fmt::Display; -use std::fs; -use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use anyhow::{Context, Result}; use clap::Args; use inquire::{Confirm, CustomType, Select}; -use katana_chain_spec::{DEV_UNALLOCATED, SettlementLayer}; +use katana_chain_spec::{SettlementLayer, DEV_UNALLOCATED}; use katana_primitives::chain::ChainId; -use katana_primitives::genesis::Genesis; use katana_primitives::genesis::allocation::DevAllocationsGenerator; +use katana_primitives::genesis::Genesis; use katana_primitives::{ContractAddress, Felt}; use lazy_static::lazy_static; use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; @@ -26,11 +23,7 @@ use tokio::runtime::Runtime; const CARTRIDGE_SN_SEPOLIA_PROVIDER: &str = "https://api.cartridge.gg/x/starknet/sepolia"; #[derive(Debug, Args)] -pub struct InitArgs { - /// The path to where the config file will be written at. - #[arg(value_name = "PATH")] - pub output_path: Option, -} +pub struct InitArgs; impl InitArgs { // TODO: @@ -52,7 +45,9 @@ impl InitArgs { chain_spec.id = ChainId::parse(&input.id)?; chain_spec.settlement = Some(settlement); - chain_spec.store(input.output_path) + katana_chain_spec::file::write(&chain_spec).context("failed to write chain spec file")?; + + Ok(()) } fn prompt(&self, rt: &Runtime) -> Result { @@ -86,7 +81,7 @@ impl InitArgs { SettlementChainOpt::Custom, ]; - let network_type = Select::new("Select settlement chain", network_opts).prompt()?; + let network_type = Select::new("Settlement chain", network_opts).prompt()?; let settlement_url = match network_type { SettlementChainOpt::Sepolia => Url::parse(CARTRIDGE_SN_SEPOLIA_PROVIDER)?, @@ -155,22 +150,12 @@ impl InitArgs { .prompt()? }; - let output_path = if let Some(path) = self.output_path.clone() { - path - } else { - CustomType::::new("Output path") - .with_default(config_path(&chain_id).map(Path)?) - .prompt()? - .0 - }; - Ok(PromptOutcome { account: account_address, settlement_contract, settlement_id: parse_cairo_short_string(&l1_chain_id)?, id: chain_id, rpc_url: settlement_url, - output_path, }) } } @@ -191,43 +176,6 @@ struct PromptOutcome { rpc_url: Url, settlement_contract: ContractAddress, - - // path at which the config file will be written at. - output_path: PathBuf, -} - -// > CONFIG_DIR/$chain_id/config.json -fn config_path(id: &str) -> Result { - Ok(config_dir(id)?.join("config").with_extension("json")) -} - -fn config_dir(id: &str) -> Result { - const KATANA_DIR: &str = "katana"; - - let _ = cairo_short_string_to_felt(id).context("Invalid id"); - let path = dirs::config_local_dir().context("unsupported OS")?.join(KATANA_DIR).join(id); - - if !path.exists() { - fs::create_dir_all(&path).expect("failed to create config directory"); - } - - Ok(path) -} - -#[derive(Debug, Clone)] -struct Path(PathBuf); - -impl FromStr for Path { - type Err = ::Err; - fn from_str(s: &str) -> std::result::Result { - PathBuf::from_str(s).map(Self) - } -} - -impl Display for Path { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.display()) - } } lazy_static! { diff --git a/crates/katana/chain-spec/Cargo.toml b/crates/katana/chain-spec/Cargo.toml index 6657aeb3e5..7b3e28cdec 100644 --- a/crates/katana/chain-spec/Cargo.toml +++ b/crates/katana/chain-spec/Cargo.toml @@ -14,7 +14,10 @@ lazy_static.workspace = true serde.workspace = true serde_json.workspace = true starknet.workspace = true +thiserror.workspace = true url.workspace = true +dirs = "6.0.0" +toml.workspace = true [dev-dependencies] similar-asserts.workspace = true diff --git a/crates/katana/chain-spec/src/file.rs b/crates/katana/chain-spec/src/file.rs new file mode 100644 index 0000000000..6a0fa75ea1 --- /dev/null +++ b/crates/katana/chain-spec/src/file.rs @@ -0,0 +1,224 @@ +use std::fs::File; +use std::io::{self, BufReader, BufWriter}; +use std::path::PathBuf; + +use katana_primitives::chain::ChainId; +use katana_primitives::genesis::json::GenesisJson; +use katana_primitives::genesis::Genesis; +use serde::{Deserialize, Serialize}; + +use crate::{ChainSpec, FeeContracts, SettlementLayer}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("OS not supported")] + UnsupportedOS, + + #[error("config directory not found for chain `{id}`")] + DirectoryNotFound { id: String }, + + #[error("failed to read config file: {0}")] + ConfigReadError(#[from] toml::ser::Error), + + #[error("failed to write config file: {0}")] + ConfigWriteError(#[from] toml::de::Error), + + #[error(transparent)] + IO(#[from] std::io::Error), + + #[error(transparent)] + GenesisJson(#[from] katana_primitives::genesis::json::GenesisJsonError), +} + +pub fn read(id: &ChainId) -> Result { + let dir = ChainConfigDir::open(id)?; + + let chain_spec: ChainSpecFile = { + let content = std::fs::read_to_string(dir.config_path())?; + toml::from_str(&content)? + }; + + let genesis: Genesis = { + let file = BufReader::new(File::open(dir.genesis_path())?); + let json: GenesisJson = serde_json::from_reader(file).map_err(io::Error::from)?; + Genesis::try_from(json)? + }; + + Ok(ChainSpec { + genesis, + id: chain_spec.id, + settlement: chain_spec.settlement, + fee_contracts: chain_spec.fee_contracts, + }) +} + +pub fn write(chain_spec: &ChainSpec) -> Result<(), Error> { + let dir = ChainConfigDir::create(&chain_spec.id)?; + + { + let cfg = ChainSpecFile { + id: chain_spec.id, + settlement: chain_spec.settlement.clone(), + fee_contracts: chain_spec.fee_contracts.clone(), + }; + + let content = toml::to_string_pretty(&cfg)?; + std::fs::write(dir.config_path(), &content)?; + } + + { + let genesis_json = GenesisJson::try_from(chain_spec.genesis.clone())?; + let file = BufWriter::new(File::create(dir.genesis_path())?); + serde_json::to_writer_pretty(file, &genesis_json).map_err(io::Error::from)?; + } + + Ok(()) +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct ChainSpecFile { + id: ChainId, + fee_contracts: FeeContracts, + #[serde(skip_serializing_if = "Option::is_none")] + settlement: Option, +} + +/// 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); + +impl ChainConfigDir { + /// Create a new config directory for the given chain ID. + /// + /// This will create the directory if it does not yet exist. + pub fn create(id: &ChainId) -> Result { + let id = id.to_string(); + let path = local_dir()?.join(id); + + if !path.exists() { + std::fs::create_dir_all(&path)?; + } + + Ok(Self(path)) + } + + /// Open an existing config directory for the given chain ID. + /// + /// This will return an error if the no config directory exists for the given chain ID. + pub fn open(id: &ChainId) -> Result { + let id = id.to_string(); + let path = local_dir()?.join(&id); + + if !path.exists() { + return Err(Error::DirectoryNotFound { id: id.clone() }); + } + + Ok(Self(path)) + } + + /// Get the path to the config file for this chain. + /// + /// > $LOCAL_DIR/$chain_id/config.toml + pub fn config_path(&self) -> PathBuf { + self.0.join("config").with_extension("toml") + } + + /// Get the path to the genesis file for this chain. + /// + /// > $LOCAL_DIR/$chain_id/genesis.json + pub fn genesis_path(&self) -> PathBuf { + self.0.join("genesis").with_extension("json") + } +} + +/// ``` +/// | -------- | --------------------------------------------- | +/// | Platform | Path | +/// | -------- | --------------------------------------------- | +/// | Linux | `$XDG_CONFIG_HOME` or `$HOME`/.config/katana | +/// | macOS | `$HOME`/Library/Application Support/katana | +/// | Windows | `{FOLDERID_LocalAppData}`/katana | +/// | -------- | --------------------------------------------- | +/// ``` +pub fn local_dir() -> Result { + Ok(dirs::config_local_dir().ok_or(Error::UnsupportedOS)?.join(KATANA_LOCAL_DIR)) +} + +#[cfg(test)] +mod tests { + use super::*; + + // To make sure the path returned by `local_dir` is always the same across + // testes and is created inside of a temp dir + fn init() { + let temp_dir = tempfile::TempDir::new().unwrap(); + let path = temp_dir.path(); + + #[cfg(target_os = "linux")] + if std::env::var("XDG_CONFIG_HOME").is_err() { + std::env::set_var("XDG_CONFIG_HOME", path); + } + + #[cfg(target_os = "macos")] + if std::env::var("HOME").is_err() { + std::env::set_var("HOME", path); + } + } + + #[test] + fn test_read_write_chainspec() { + init(); + + let chain_spec = ChainSpec::default(); + let id = chain_spec.id; + + write(&chain_spec).unwrap(); + let read_spec = read(&id).unwrap(); + + assert_eq!(chain_spec.id, read_spec.id); + assert_eq!(chain_spec.fee_contracts, read_spec.fee_contracts); + assert_eq!(chain_spec.settlement, read_spec.settlement); + } + + #[test] + fn test_chain_config_dir() { + init(); + + let chain_id = ChainId::parse("test").unwrap(); + + // Test creation + let config_dir = ChainConfigDir::create(&chain_id).unwrap(); + assert!(config_dir.0.exists()); + + // Test opening existing dir + let opened_dir = ChainConfigDir::open(&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(&bad_id), Err(Error::DirectoryNotFound { .. }))); + } + + #[test] + fn test_local_dir() { + init(); + + let dir = local_dir().unwrap(); + assert!(dir.ends_with(KATANA_LOCAL_DIR)); + } + + #[test] + fn test_config_paths() { + init(); + + let chain_id = ChainId::parse("test").unwrap(); + let config_dir = ChainConfigDir::create(&chain_id).unwrap(); + + assert!(config_dir.config_path().ends_with("config.toml")); + assert!(config_dir.genesis_path().ends_with("genesis.json")); + } +} diff --git a/crates/katana/chain-spec/src/lib.rs b/crates/katana/chain-spec/src/lib.rs index bbfbec8d94..15eb2f7261 100644 --- a/crates/katana/chain-spec/src/lib.rs +++ b/crates/katana/chain-spec/src/lib.rs @@ -1,10 +1,6 @@ use std::collections::BTreeMap; -use std::fs::File; -use std::io::BufReader; -use std::path::{Path, PathBuf}; use alloy_primitives::U256; -use anyhow::{Context, Result}; use katana_primitives::block::{Block, Header}; use katana_primitives::chain::ChainId; use katana_primitives::class::ClassHash; @@ -18,7 +14,6 @@ use katana_primitives::genesis::constant::{ DEFAULT_STRK_FEE_TOKEN_ADDRESS, DEFAULT_UDC_ADDRESS, ERC20_DECIMAL_STORAGE_SLOT, ERC20_NAME_STORAGE_SLOT, ERC20_SYMBOL_STORAGE_SLOT, ERC20_TOTAL_SUPPLY_STORAGE_SLOT, }; -use katana_primitives::genesis::json::GenesisJson; use katana_primitives::genesis::Genesis; use katana_primitives::state::StateUpdatesWithClasses; use katana_primitives::utils::split_u256; @@ -29,6 +24,8 @@ use serde::{Deserialize, Serialize}; use starknet::core::utils::cairo_short_string_to_felt; use url::Url; +pub mod file; + /// The rollup chain specification. #[derive(Debug, Clone)] #[cfg_attr(test, derive(PartialEq))] @@ -62,7 +59,7 @@ pub struct FeeContracts { #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq))] -#[serde(tag = "type", rename_all = "camelCase")] +#[serde(rename_all = "kebab-case")] pub enum SettlementLayer { Ethereum { // The id of the settlement chain. @@ -98,40 +95,6 @@ pub enum SettlementLayer { ////////////////////////////////////////////////////////////// impl ChainSpec { - pub fn load>(path: P) -> Result { - let content = std::fs::read_to_string(&path)?; - let cs = serde_json::from_str::(&content)?; - - let file = File::open(&cs.genesis).context("failed to open genesis file")?; - - // the genesis file is stored as its JSON representation - let genesis_json: GenesisJson = serde_json::from_reader(BufReader::new(file))?; - let genesis = Genesis::try_from(genesis_json)?; - - Ok(Self { genesis, id: cs.id, settlement: cs.settlement, fee_contracts: cs.fee_contracts }) - } - - pub fn store>(self, path: P) -> anyhow::Result<()> { - let cfg_path = path.as_ref(); - let mut genesis_path = cfg_path.to_path_buf(); - genesis_path.set_file_name("genesis.json"); - - let stored = ChainSpecFile { - id: self.id, - genesis: genesis_path, - settlement: self.settlement, - fee_contracts: self.fee_contracts, - }; - - // convert the genesis to its JSON representation and store it - let genesis_json = GenesisJson::try_from(self.genesis)?; - - serde_json::to_writer_pretty(File::create(cfg_path)?, &stored)?; - serde_json::to_writer_pretty(File::create(stored.genesis)?, &genesis_json)?; - - Ok(()) - } - pub fn block(&self) -> Block { let header = Header { state_diff_length: 0, @@ -204,15 +167,6 @@ impl Default for ChainSpec { } } -#[derive(Debug, Serialize, Deserialize)] -struct ChainSpecFile { - id: ChainId, - fee_contracts: FeeContracts, - #[serde(skip_serializing_if = "Option::is_none")] - settlement: Option, - genesis: PathBuf, -} - lazy_static! { /// The default chain specification in dev mode. pub static ref DEV: ChainSpec = { @@ -365,20 +319,6 @@ mod tests { use super::*; - #[test] - fn chainspec_load_store_rt() { - let chainspec = ChainSpec::default(); - - // Create a temporary file and store the ChainSpec - let temp = tempfile::NamedTempFile::new().unwrap(); - chainspec.clone().store(temp.path()).unwrap(); - - // Load the ChainSpec back from the file - let loaded_chainspec = ChainSpec::load(temp.path()).unwrap(); - - similar_asserts::assert_eq!(chainspec, loaded_chainspec); - } - #[test] fn genesis_block_and_state_updates() { // setup initial states to test diff --git a/crates/katana/cli/src/args.rs b/crates/katana/cli/src/args.rs index 2dce318c2a..4621cfb022 100644 --- a/crates/katana/cli/src/args.rs +++ b/crates/katana/cli/src/args.rs @@ -20,6 +20,7 @@ use katana_node::config::rpc::RpcConfig; #[cfg(feature = "server")] use katana_node::config::rpc::{RpcModuleKind, RpcModulesList}; use katana_node::config::{Config, SequencingConfig}; +use katana_primitives::chain::ChainId; use katana_primitives::genesis::allocation::DevAllocationsGenerator; use katana_primitives::genesis::constant::DEFAULT_PREFUNDED_ACCOUNT_BALANCE; use serde::{Deserialize, Serialize}; @@ -44,7 +45,8 @@ pub struct NodeArgs { /// Path to the chain configuration file. #[arg(long, hide = true)] - pub chain: Option, + #[arg(value_parser = ChainId::parse)] + pub chain: Option, /// Disable auto and interval mining, and mine on demand instead via an endpoint. #[arg(long)] @@ -236,9 +238,12 @@ impl NodeArgs { } fn chain_spec(&self) -> Result> { - if let Some(path) = &self.chain { - let mut chain_spec = ChainSpec::load(path).context("failed to load chain spec")?; + if let Some(id) = &self.chain { + use katana_chain_spec::file; + + let mut chain_spec = file::read(id).context("failed to load chain spec")?; chain_spec.genesis.sequencer_address = *DEFAULT_SEQUENCER_ADDRESS; + Ok(Arc::new(chain_spec)) } // exclusively for development mode