diff --git a/crates/cli/src/command/fuzz.rs b/crates/cli/src/command/fuzz.rs index 3ca95a95f..5d5b91f3c 100644 --- a/crates/cli/src/command/fuzz.rs +++ b/crates/cli/src/command/fuzz.rs @@ -11,6 +11,7 @@ use trident_client::___private::TestGenerator; use crate::command::check_fuzz_test_exists; use crate::command::check_fuzz_test_not_exists; use crate::command::check_trident_uninitialized; +use crate::command::ensure_running_from_trident_tests; use crate::command::get_project_root_for_fuzz; use crate::command::is_anchor_project; use crate::command::validate_program_name_usage; @@ -139,11 +140,13 @@ pub(crate) async fn fuzz(subcmd: FuzzCommand) { exit_code, seed, } => { + ensure_running_from_trident_tests(&root)?; let commander = Commander::new(&root); commander.run(target, exit_code, seed).await?; } FuzzCommand::Debug { target, seed } => { + ensure_running_from_trident_tests(&root)?; let commander = Commander::new(&root); commander.run_debug(target, seed).await?; diff --git a/crates/cli/src/command/mod.rs b/crates/cli/src/command/mod.rs index d9263d88a..bc0a0b2c7 100644 --- a/crates/cli/src/command/mod.rs +++ b/crates/cli/src/command/mod.rs @@ -139,6 +139,16 @@ fn check_trident_uninitialized(root: &str) { } } +#[throws] +fn ensure_running_from_trident_tests(root: &str) { + let trident_tests_dir = Path::new(root).join(TESTS_WORKSPACE_DIRECTORY); + let current_dir = std::env::current_dir()?; + + if !current_dir.starts_with(&trident_tests_dir) { + bail!("Run this command from trident-tests.",); + } +} + #[throws] fn check_fuzz_test_exists(root: &str, fuzz_test_name: &str) { let fuzz_test_dir = Path::new(&root) diff --git a/crates/client/src/commander/fuzz.rs b/crates/client/src/commander/fuzz.rs index 7ddf32177..10977f13e 100644 --- a/crates/client/src/commander/fuzz.rs +++ b/crates/client/src/commander/fuzz.rs @@ -20,7 +20,7 @@ impl Commander { exit_code_mode: Option, seed: Option, ) { - let config = TridentConfig::new(); + let config = TridentConfig::try_new()?; if config.get_metrics() { std::env::set_var("FUZZING_METRICS", "true"); @@ -184,7 +184,7 @@ impl Commander { #[throws] pub async fn run_debug(&self, target: String, seed: String) { - let config = TridentConfig::new(); + let config = TridentConfig::try_new()?; if config.get_metrics() { if config.get_metrics_json() { diff --git a/crates/client/src/commander/mod.rs b/crates/client/src/commander/mod.rs index 6d32f91c8..7c67198a2 100644 --- a/crates/client/src/commander/mod.rs +++ b/crates/client/src/commander/mod.rs @@ -19,9 +19,9 @@ mod fuzz; #[derive(Error, Debug)] pub enum Error { - #[error("{0:?}")] + #[error("{0}")] Io(#[from] io::Error), - #[error("{0:?}")] + #[error("{0}")] Utf8(#[from] FromUtf8Error), #[error("build programs failed")] BuildProgramsFailed, @@ -33,8 +33,10 @@ pub enum Error { Coverage(#[from] crate::coverage::CoverageError), #[error("Cannot find the trident-tests directory in the current workspace")] BadWorkspace, - #[error("{0:?}")] + #[error("{0}")] Anyhow(#[from] anyhow::Error), + #[error("Invalid Trident.toml configuration: {0}")] + Config(#[from] trident_config::Error), } /// `Commander` allows you to start localnet, build programs, diff --git a/crates/config/src/constants.rs b/crates/config/src/constants.rs index a876287ca..9c0117a4f 100644 --- a/crates/config/src/constants.rs +++ b/crates/config/src/constants.rs @@ -1,5 +1,6 @@ // tomls pub(crate) const TRIDENT_TOML: &str = "Trident.toml"; +pub(crate) const TESTS_WORKSPACE_DIRECTORY: &str = "trident-tests"; // fuzz pub(crate) const DEFAULT_LOOPCOUNT: u64 = 0; diff --git a/crates/config/src/coverage.rs b/crates/config/src/coverage.rs index 80f8d7ea8..19ff7c573 100644 --- a/crates/config/src/coverage.rs +++ b/crates/config/src/coverage.rs @@ -5,6 +5,7 @@ use crate::constants::DEFAULT_COVERAGE_SERVER_PORT; use crate::constants::DEFAULT_LOOPCOUNT; #[derive(Debug, Deserialize, Clone)] +#[serde(deny_unknown_fields)] pub struct Coverage { pub enable: Option, pub server_port: Option, diff --git a/crates/config/src/fuzz.rs b/crates/config/src/fuzz.rs index fbe7a89c0..bac3ce3f2 100644 --- a/crates/config/src/fuzz.rs +++ b/crates/config/src/fuzz.rs @@ -2,6 +2,7 @@ use crate::coverage::Coverage; use crate::metrics::Metrics; use crate::regression::Regression; use crate::utils::resolve_path; +use crate::Error; use base64::prelude::BASE64_STANDARD; use base64::Engine; use serde::Deserialize; @@ -13,6 +14,7 @@ use std::fs; use std::str::FromStr; #[derive(Debug, Deserialize, Clone, Default)] +#[serde(deny_unknown_fields)] pub struct Fuzz { metrics: Option, regression: Option, @@ -55,15 +57,79 @@ impl Fuzz { self.coverage.clone().unwrap_or_default() } - pub fn get_forks(&self) -> Vec { + pub fn get_forks(&self) -> Result, Error> { match self.fork.as_ref() { - Some(forks) => forks.iter().map(FuzzFork::from).collect(), - None => Vec::default(), + Some(forks) => forks.iter().map(FuzzFork::try_from_raw).collect(), + None => Ok(Vec::default()), } } + + /// Lightweight preflight validation used by CLI startup checks. + /// This validates addresses and file path existence without loading + /// full program binaries/account payloads into memory. + pub fn validate_preflight(&self) -> Result<(), Error> { + if let Some(programs) = &self.programs { + for program in programs { + let _ = Pubkey::from_str(&program.address).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Cannot parse the program address: {}", + program.address + )) + })?; + if let Some(authority) = program.upgrade_authority.as_ref() { + let _ = Pubkey::from_str(authority).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Cannot parse upgrade authority: {}", + authority + )) + })?; + } + + let path = resolve_path(&program.program)?; + if !path.exists() { + return Err(Error::Anyhow(anyhow::anyhow!( + "Failed to read file: {}", + program.program + ))); + } + } + } + + if let Some(accounts) = &self.accounts { + for account in accounts { + let _ = Pubkey::from_str(&account.address).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Cannot parse account address: {}", + account.address + )) + })?; + let path = resolve_path(&account.filename)?; + if !path.exists() { + return Err(Error::Anyhow(anyhow::anyhow!( + "Failed to read file: {}", + account.filename + ))); + } + } + } + + if let Some(forks) = &self.fork { + for fork in forks { + let _ = Pubkey::from_str(&fork.address).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Cannot parse fork address: {}", + fork.address + )) + })?; + } + } + + Ok(()) + } } #[derive(Debug, Deserialize, Clone)] +#[serde(deny_unknown_fields)] pub struct _FuzzProgram { pub address: String, pub upgrade_authority: Option, @@ -71,12 +137,14 @@ pub struct _FuzzProgram { } #[derive(Debug, Deserialize, Clone)] +#[serde(deny_unknown_fields)] pub struct _FuzzAccount { pub address: String, pub filename: String, } #[derive(Debug, Deserialize, Clone)] +#[serde(deny_unknown_fields)] pub struct _FuzzFork { pub address: String, pub cluster: String, @@ -142,6 +210,22 @@ impl From<&_FuzzFork> for FuzzFork { } } } +impl FuzzFork { + pub fn try_from_raw(value: &_FuzzFork) -> Result { + let address = Pubkey::from_str(&value.address).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Cannot parse fork address: {}", + value.address + )) + })?; + + Ok(FuzzFork { + address, + cluster: FuzzCluster::parse(&value.cluster), + overwrite: value.overwrite, + }) + } +} #[derive(Debug, Deserialize, Clone)] pub struct FuzzProgram { @@ -160,7 +244,8 @@ impl From<&_FuzzProgram> for FuzzProgram { .as_ref() .map(|upgrade_authority| Pubkey::from_str(upgrade_authority).unwrap()); - let path = resolve_path(program_path); + let path = resolve_path(program_path) + .unwrap_or_else(|_| panic!("Failed to resolve path: {}", program_path)); let program_data = fs::read(path).unwrap_or_else(|_| panic!("Failed to read file: {}", program_path)); @@ -175,6 +260,37 @@ impl From<&_FuzzProgram> for FuzzProgram { } } } +impl FuzzProgram { + pub fn try_from_raw(value: &_FuzzProgram) -> Result { + let path = resolve_path(&value.program)?; + let program_data = fs::read(path).map_err(|_| { + Error::Anyhow(anyhow::anyhow!("Failed to read file: {}", value.program)) + })?; + + let address = Pubkey::from_str(&value.address).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Cannot parse the program address: {}", + value.address + )) + })?; + + let upgrade_authority = match value.upgrade_authority.as_ref() { + Some(authority) => Some(Pubkey::from_str(authority).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Cannot parse upgrade authority: {}", + authority + )) + })?), + None => None, + }; + + Ok(FuzzProgram { + address, + upgrade_authority, + data: program_data, + }) + } +} #[derive(Debug, Deserialize, Clone)] pub struct FuzzAccount { @@ -186,7 +302,8 @@ impl From<&_FuzzAccount> for FuzzAccount { fn from(_f: &_FuzzAccount) -> Self { let account_path = &_f.filename; - let path = resolve_path(account_path); + let path = resolve_path(account_path) + .unwrap_or_else(|_| panic!("Failed to resolve path: {}", account_path)); let file_content = fs::read_to_string(path) .unwrap_or_else(|_| panic!("Failed to read file: {}", account_path)); @@ -224,14 +341,69 @@ impl From<&_FuzzAccount> for FuzzAccount { FuzzAccount { pubkey, account } } } +impl FuzzAccount { + pub fn try_from_raw(value: &_FuzzAccount) -> Result { + let path = resolve_path(&value.filename)?; + let file_content = fs::read_to_string(path).map_err(|_| { + Error::Anyhow(anyhow::anyhow!("Failed to read file: {}", value.filename)) + })?; + + let account_raw: FuzzAccountRaw = serde_json::from_str(&file_content).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Failed to parse JSON from file: {}", + value.filename + )) + })?; + + let pubkey = Pubkey::from_str(&account_raw.pubkey).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Cannot convert address for: {}", + account_raw.pubkey + )) + })?; + + let owner_address = Pubkey::from_str(&account_raw.account.owner).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Cannot convert address for owner: {}", + account_raw.account.owner + )) + })?; + + let data_base_64 = account_raw.account.data.first().ok_or_else(|| { + Error::Anyhow(anyhow::anyhow!( + "Cannot read base64 data for account: {}", + account_raw.pubkey + )) + })?; + + let data = BASE64_STANDARD.decode(data_base_64).map_err(|_| { + Error::Anyhow(anyhow::anyhow!( + "Failed to decode base64 data of {}", + value.filename + )) + })?; + + let account = AccountSharedData::create( + account_raw.account.lamports, + data, + owner_address, + account_raw.account.executable, + account_raw.account.rent_epoch, + ); + + Ok(FuzzAccount { pubkey, account }) + } +} #[derive(Debug, Deserialize, Clone)] +#[serde(deny_unknown_fields)] pub struct FuzzAccountRaw { pub pubkey: String, pub account: AccountRaw, } #[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] pub struct AccountRaw { pub lamports: u64, pub data: Vec, diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index a765ea944..1473218af 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -25,15 +25,16 @@ mod regression; pub enum Error { #[error("invalid workspace")] BadWorkspace, - #[error("{0:?}")] + #[error("{0}")] Anyhow(#[from] anyhow::Error), - #[error("{0:?}")] + #[error("{0}")] Io(#[from] io::Error), - #[error("{0:?}")] + #[error("{0}")] Toml(#[from] toml::de::Error), } #[derive(Debug, Deserialize, Clone)] +#[serde(deny_unknown_fields)] pub struct TridentConfig { pub fuzz: Option, } @@ -46,12 +47,23 @@ impl Default for TridentConfig { impl TridentConfig { pub fn new() -> Self { - let root = discover_root().expect("failed to find the root folder"); - let s = fs::read_to_string(root.join(TRIDENT_TOML).as_path()) - .expect("failed to read the Trident config file"); - let _config: TridentConfig = - toml::from_str(&s).expect("failed to parse the Trident config file"); - _config + Self::try_new().unwrap_or_else(|e| panic!("failed to load Trident config: {e}")) + } + + pub fn try_new() -> Result { + let root = discover_root()?; + let s = fs::read_to_string(root.join(TRIDENT_TOML).as_path())?; + let config: TridentConfig = toml::from_str(&s)?; + config.validate_preflight()?; + + Ok(config) + } + + pub fn validate_preflight(&self) -> Result<(), Error> { + if let Some(fuzz) = self.fuzz.as_ref() { + fuzz.validate_preflight()?; + } + Ok(()) } // -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* @@ -100,36 +112,51 @@ impl TridentConfig { } pub fn programs(&self) -> Vec { + self.try_programs() + .unwrap_or_else(|e| panic!("failed to parse programs in Trident.toml: {e}")) + } + + pub fn try_programs(&self) -> Result, Error> { self.fuzz .as_ref() .map(|fuzz| { if let Some(programs) = &fuzz.programs { - programs.iter().map(FuzzProgram::from).collect() + programs.iter().map(FuzzProgram::try_from_raw).collect() } else { - Vec::default() + Ok(Vec::default()) } }) - .unwrap_or_default() + .unwrap_or_else(|| Ok(Vec::default())) } pub fn accounts(&self) -> Vec { + self.try_accounts() + .unwrap_or_else(|e| panic!("failed to parse accounts in Trident.toml: {e}")) + } + + pub fn try_accounts(&self) -> Result, Error> { self.fuzz .as_ref() .map(|fuzz| { if let Some(accounts) = &fuzz.accounts { - accounts.iter().map(FuzzAccount::from).collect() + accounts.iter().map(FuzzAccount::try_from_raw).collect() } else { - Vec::default() + Ok(Vec::default()) } }) - .unwrap_or_default() + .unwrap_or_else(|| Ok(Vec::default())) } pub fn forks(&self) -> Vec { + self.try_forks() + .unwrap_or_else(|e| panic!("failed to parse forks in Trident.toml: {e}")) + } + + pub fn try_forks(&self) -> Result, Error> { self.fuzz .as_ref() .map(|fuzz| fuzz.get_forks()) - .unwrap_or_default() + .unwrap_or_else(|| Ok(Vec::default())) } /// Get forked accounts from cache (silent, no RPC calls). @@ -137,7 +164,13 @@ impl TridentConfig { /// This should be called after `fork()` has been executed to ensure /// all accounts are cached. Used by worker threads during parallel fuzzing. pub fn get_forked_accounts(&self) -> Vec<(Pubkey, AccountSharedData)> { - let forks = self.forks(); + let forks = match self.try_forks() { + Ok(forks) => forks, + Err(e) => { + eprintln!("Warning: Failed to parse forks from config: {}", e); + return Vec::new(); + } + }; if forks.is_empty() { return Vec::new(); } @@ -155,7 +188,7 @@ impl TridentConfig { /// /// This should be called ONCE in the main thread before parallel fuzzing starts. pub fn fork(&self) -> Result<(), Box> { - let forks = self.forks(); + let forks = self.try_forks()?; if forks.is_empty() { return Ok(()); } diff --git a/crates/config/src/metrics.rs b/crates/config/src/metrics.rs index 3ed362b9f..3e07614c4 100644 --- a/crates/config/src/metrics.rs +++ b/crates/config/src/metrics.rs @@ -1,6 +1,7 @@ use serde::Deserialize; #[derive(Debug, Deserialize, Clone, Default)] +#[serde(deny_unknown_fields)] pub(crate) struct Metrics { pub(crate) enabled: Option, pub(crate) json: Option, diff --git a/crates/config/src/regression.rs b/crates/config/src/regression.rs index 577f8fe2a..ad26015d2 100644 --- a/crates/config/src/regression.rs +++ b/crates/config/src/regression.rs @@ -1,6 +1,7 @@ use serde::Deserialize; #[derive(Debug, Deserialize, Clone, Default)] +#[serde(deny_unknown_fields)] pub(crate) struct Regression { pub(crate) enabled: Option, } diff --git a/crates/config/src/utils.rs b/crates/config/src/utils.rs index 574557d7c..e16fdf87c 100644 --- a/crates/config/src/utils.rs +++ b/crates/config/src/utils.rs @@ -1,3 +1,4 @@ +use crate::constants::TESTS_WORKSPACE_DIRECTORY; use crate::constants::TRIDENT_TOML; use crate::Error; @@ -7,14 +8,12 @@ use std::env; use std::path::Path; use std::path::PathBuf; -pub(crate) fn resolve_path(filename: &str) -> PathBuf { +pub(crate) fn resolve_path(filename: &str) -> Result { let path = Path::new(filename); if path.is_absolute() { - path.to_path_buf() + Ok(path.to_path_buf()) } else { - discover_root() - .map(|cwd| cwd.join(path)) - .unwrap_or_else(|_| panic!("Failed to resolve relative path: {}", path.display())) + discover_root().map(|cwd| cwd.join(path)) } } @@ -24,6 +23,18 @@ pub fn discover_root() -> Result { let current_dir = env::current_dir()?; let mut dir = Some(current_dir.as_path()); while let Some(cwd) = dir { + // 1) Direct layout: /Trident.toml + let direct_trident_toml = cwd.join(TRIDENT_TOML); + if direct_trident_toml.exists() { + return Ok(PathBuf::from(cwd)); + } + + // 2) Workspace layout: /trident-tests/Trident.toml + let nested_trident_toml = cwd.join(TESTS_WORKSPACE_DIRECTORY).join(TRIDENT_TOML); + if nested_trident_toml.exists() { + return Ok(cwd.join(TESTS_WORKSPACE_DIRECTORY)); + } + for file in std::fs::read_dir(cwd) .with_context(|| format!("Error reading the directory with path: {}", cwd.display()))? { diff --git a/crates/fuzz/src/trident/flow_executor.rs b/crates/fuzz/src/trident/flow_executor.rs index 9b800d205..c52699594 100644 --- a/crates/fuzz/src/trident/flow_executor.rs +++ b/crates/fuzz/src/trident/flow_executor.rs @@ -283,8 +283,17 @@ pub trait FlowExecutor: Send + 'static + Sized { /// This is called once at the beginning to avoid race conditions with RPC calls /// and cache writes when multiple threads are spawned. fn ensure_forks_processed() { - let config = TridentConfig::new(); - let _ = config.fork(); + let config = match TridentConfig::try_new() { + Ok(config) => config, + Err(e) => { + eprintln!("Invalid Trident.toml configuration: {}", e); + std::process::exit(FuzzRunExit::RuntimeFailure.code()); + } + }; + if let Err(e) = config.fork() { + eprintln!("Failed to process fork entries from Trident.toml: {}", e); + std::process::exit(FuzzRunExit::RuntimeFailure.code()); + } } /// Sets up a global panic handler that captures panic location information. diff --git a/crates/fuzz/src/trident/mod.rs b/crates/fuzz/src/trident/mod.rs index 49de06481..c9b6967c5 100644 --- a/crates/fuzz/src/trident/mod.rs +++ b/crates/fuzz/src/trident/mod.rs @@ -109,11 +109,24 @@ impl Trident { } fn new_client() -> TridentSVM { - let config = TridentConfig::new(); + let config = match TridentConfig::try_new() { + Ok(config) => config, + Err(e) => { + eprintln!("Invalid Trident.toml configuration: {}", e); + std::process::exit(1); + } + }; let mut genesis_accounts = Vec::new(); // Add programs from config - for program in config.programs() { + let programs = match config.try_programs() { + Ok(programs) => programs, + Err(e) => { + eprintln!("Invalid Trident.toml configuration: {}", e); + std::process::exit(1); + } + }; + for program in programs { let accounts = TridentAccountSharedData::loader_v3_program( program.address, &program.data, @@ -123,7 +136,14 @@ impl Trident { } // Add regular accounts from config - for account_config in config.accounts() { + let accounts = match config.try_accounts() { + Ok(accounts) => accounts, + Err(e) => { + eprintln!("Invalid Trident.toml configuration: {}", e); + std::process::exit(1); + } + }; + for account_config in accounts { let account = TridentAccountSharedData::new( account_config.pubkey, account_config.account.clone(),