1use crate::{
2 cli::{
3 config::OriginFuzzingOption::{
4 DisableOriginFuzzing,
5 EnableOriginFuzzing,
6 },
7 ui::logger::LAST_SEED_FILENAME,
8 },
9 contract::{
10 remote::{
11 BalanceOf,
12 ContractSetup,
13 },
14 runtime::Runtime,
15 },
16 fuzzer::fuzz::MAX_MESSAGES_PER_EXEC,
17 instrumenter::path::InstrumentedPath,
18 EmptyResult,
19 ResultOf,
20};
21use anyhow::{
22 bail,
23 Context,
24};
25use frame_support::weights::Weight;
26use serde_derive::{
27 Deserialize,
28 Serialize,
29};
30use sp_core::crypto::AccountId32;
31use std::{
32 fmt::{
33 Display,
34 Formatter,
35 },
36 fs,
37 fs::File,
38 io::Write,
39 path::PathBuf,
40};
41
42#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
43pub struct Configuration {
44 pub cores: Option<u8>,
46 pub use_honggfuzz: bool,
48 pub deployer_address: Option<AccountId32>,
50 pub max_messages_per_exec: Option<usize>,
52 pub report_path: Option<PathBuf>,
54 pub fuzz_origin: bool,
57 pub default_gas_limit: Option<Weight>,
59 pub storage_deposit_limit: Option<String>,
62 pub instantiate_initial_value: Option<String>,
65 pub constructor_payload: Option<String>,
72 pub verbose: bool,
74 pub instrumented_contract_path: Option<InstrumentedPath>,
78 pub fuzz_output: Option<PathBuf>,
81 pub show_ui: bool,
83 pub catch_trapped_contract: bool,
87}
88
89impl Configuration {
90 pub fn deployer_address(&self) -> &AccountId32 {
91 self.deployer_address
92 .as_ref()
93 .unwrap_or(&ContractSetup::DEFAULT_DEPLOYER)
94 }
95}
96
97impl Default for Configuration {
98 fn default() -> Self {
99 Self {
100 cores: Some(1),
101 use_honggfuzz: false,
102 fuzz_origin: false,
103 deployer_address: Some(ContractSetup::DEFAULT_DEPLOYER),
104 max_messages_per_exec: Some(MAX_MESSAGES_PER_EXEC),
105 report_path: Some(PathBuf::from("output/coverage_report")),
106 default_gas_limit: Some(ContractSetup::DEFAULT_GAS_LIMIT),
107 storage_deposit_limit: Some("100000000000".into()),
108 instantiate_initial_value: None,
109 constructor_payload: None,
110 verbose: true,
111 instrumented_contract_path: Some(InstrumentedPath::default()),
112 fuzz_output: Some(PathBuf::from("output")),
113 show_ui: true,
114 catch_trapped_contract: false,
115 }
116 }
117}
118
119#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
120pub enum OriginFuzzingOption {
121 EnableOriginFuzzing,
122 #[default]
123 DisableOriginFuzzing,
124}
125
126#[derive(Copy, Clone, Debug)]
127pub enum PFiles {
128 CoverageTracePath,
129 AllowlistPath,
130 DictPath,
131 CorpusPath,
132 AFLLog,
133 LastSeed,
134}
135#[derive(Clone, Debug)]
136pub struct PhinkFiles {
137 output: PathBuf,
138}
139impl Display for PhinkFiles {
140 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
141 f.write_str(self.output.to_str().unwrap())
142 }
143}
144
145impl PhinkFiles {
146 const PHINK_PATH: &str = "phink";
147
148 pub fn new(output: PathBuf) -> Self {
149 Self { output }
150 }
151 pub fn new_by_ref(output: &PathBuf) -> Self {
152 Self {
153 output: output.to_owned(),
154 }
155 }
156 pub fn output(self) -> PathBuf {
157 self.output
158 }
159
160 pub fn make_all(self) -> Self {
161 fs::create_dir_all(self.clone().output().join(Self::PHINK_PATH)).unwrap();
162 self
163 }
164
165 pub fn path(&self, file: PFiles) -> PathBuf {
166 match file {
167 PFiles::CoverageTracePath => self.output.join(Self::PHINK_PATH).join("traces.cov"),
168 PFiles::AllowlistPath => self.output.join(Self::PHINK_PATH).join("allowlist.txt"),
169 PFiles::DictPath => self.output.join(Self::PHINK_PATH).join("selectors.dict"),
170 PFiles::CorpusPath => self.output.join(Self::PHINK_PATH).join("corpus"),
171 PFiles::AFLLog => {
172 self.output
173 .join(Self::PHINK_PATH)
174 .join("logs")
175 .join("afl.log")
176 }
177 PFiles::LastSeed => {
178 self.output
179 .join(Self::PHINK_PATH)
180 .join("logs")
181 .join(LAST_SEED_FILENAME)
182 }
183 }
184 }
185}
186
187impl TryFrom<String> for Configuration {
188 type Error = anyhow::Error;
189 fn try_from(config_str: String) -> ResultOf<Self> {
190 let config: Configuration = match toml::from_str(&config_str) {
191 Ok(config) => config,
192 Err(e) => bail!("Can't parse config: {e}"),
193 };
194
195 if Configuration::parse_balance(&config.storage_deposit_limit.clone()).is_none() {
196 bail!("Cannot parse string to `u128` for `storage_deposit_limit`, check your configuration file");
197 }
198
199 Ok(config)
200 }
201}
202
203impl TryFrom<&PathBuf> for Configuration {
204 type Error = anyhow::Error;
205 fn try_from(path: &PathBuf) -> ResultOf<Self> {
206 match fs::read_to_string(path) {
207 Ok(config) => config.try_into(),
208 Err(err) => bail!("🚫 Can't read config: {err}"),
209 }
210 }
211}
212
213impl Configuration {
214 pub fn should_fuzz_origin(&self) -> OriginFuzzingOption {
215 match self.fuzz_origin {
216 true => EnableOriginFuzzing,
217 false => DisableOriginFuzzing,
218 }
219 }
220
221 pub fn instrumented_contract(&self) -> PathBuf {
222 self.instrumented_contract_path
223 .clone()
224 .unwrap_or_default()
225 .path
226 }
227
228 pub fn save_as_toml(&self, to: &str) -> EmptyResult {
229 let toml_str =
230 toml::to_string(self).with_context(|| "Couldn't serialize to toml".to_string())?;
231 let mut file = File::create(to).with_context(|| format!("Couldn't create file {to}"))?;
232 file.write_all(toml_str.as_bytes())?;
233 Ok(())
234 }
235
236 pub fn parse_balance(value: &Option<String>) -> Option<BalanceOf<Runtime>> {
237 value.clone().and_then(|s| s.parse::<u128>().ok())
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use std::str::FromStr;
248
249 fn create_test_config() -> Configuration {
250 Configuration {
251 cores: Some(2),
252 use_honggfuzz: true,
253 deployer_address: Some(
254 AccountId32::from_str("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY").unwrap(),
255 ),
256 max_messages_per_exec: Some(10),
257 report_path: Some(PathBuf::from("/tmp/report")),
258 fuzz_origin: true,
259 catch_trapped_contract: false,
260 default_gas_limit: Some(Weight::from_parts(100_000_000_000, 0)),
261 storage_deposit_limit: Some("1000000000".into()),
262 instantiate_initial_value: Some("500".into()),
263 verbose: true,
264 instrumented_contract_path: Some(InstrumentedPath::from("/tmp/instrumented")),
265 fuzz_output: Some(PathBuf::from("/tmp/fuzz_output")),
266 show_ui: false,
267 ..Default::default()
268 }
269 }
270
271 #[test]
272 fn test_default_configuration() {
273 let default_config = Configuration::default();
274 assert_eq!(default_config.cores, Some(1));
275 assert!(!default_config.use_honggfuzz);
276 assert!(!default_config.fuzz_origin);
277 assert_eq!(
278 default_config.max_messages_per_exec,
279 Some(MAX_MESSAGES_PER_EXEC)
280 );
281 assert_eq!(
282 default_config.report_path,
283 Some(PathBuf::from("output/coverage_report"))
284 );
285 assert_eq!(
286 default_config.default_gas_limit,
287 Some(ContractSetup::DEFAULT_GAS_LIMIT)
288 );
289 assert_eq!(
290 default_config.storage_deposit_limit,
291 Some("100000000000".into())
292 );
293 assert!(default_config.show_ui);
294 }
295
296 #[test]
297 fn test_should_fuzz_origin() {
298 let mut config = create_test_config();
299 assert_eq!(config.should_fuzz_origin(), EnableOriginFuzzing);
300
301 config.fuzz_origin = false;
302 assert_eq!(config.should_fuzz_origin(), DisableOriginFuzzing);
303 }
304
305 #[test]
306 fn test_parse_balance() {
307 assert_eq!(Configuration::parse_balance(&Some("100".into())), Some(100));
308 assert_eq!(Configuration::parse_balance(&Some("0".into())), Some(0));
309 assert_eq!(
310 Configuration::parse_balance(&Some("18446744073709551615".into())),
311 Some(18446744073709551615)
312 );
313 assert_eq!(Configuration::parse_balance(&None), None);
314 assert_eq!(Configuration::parse_balance(&Some("invalid".into())), None);
315 }
316
317 #[test]
318 fn test_try_from_string() {
319 let config_str = r#"
320 cores = 4
321 use_honggfuzz = true
322 fuzz_origin = true
323 max_messages_per_exec = 20
324 storage_deposit_limit = "200000000000"
325 verbose = false
326 catch_trapped_contract = true
327 show_ui = true
328 "#;
329
330 let config: Configuration = config_str.to_string().try_into().unwrap();
331 assert_eq!(config.cores, Some(4));
332 assert!(config.use_honggfuzz);
333 assert!(config.fuzz_origin);
334 assert_eq!(config.max_messages_per_exec, Some(20));
335 assert_eq!(config.storage_deposit_limit, Some("200000000000".into()));
336 assert!(!config.verbose);
337 assert!(config.show_ui);
338 }
339
340 #[test]
341 fn test_try_from_string_invalid_config() {
342 let invalid_config_str = r#"
343 cores = "invalid"
344 storage_deposit_limit = "not_a_number"
345 "#;
346
347 let result: ResultOf<Configuration> = invalid_config_str.to_string().try_into();
348 assert!(result.is_err());
349 }
350
351 #[test]
352 fn test_phink_files() {
353 let output = PathBuf::from("/tmp/phink_output");
354 let phink_files = PhinkFiles::new(output.clone());
355
356 assert_eq!(
357 phink_files.path(PFiles::CoverageTracePath),
358 output.join("phink").join("traces.cov")
359 );
360 assert_eq!(
361 phink_files.path(PFiles::AllowlistPath),
362 output.join("phink").join("allowlist.txt")
363 );
364 assert_eq!(
365 phink_files.path(PFiles::DictPath),
366 output.join("phink").join("selectors.dict")
367 );
368 assert_eq!(
369 phink_files.path(PFiles::CorpusPath),
370 output.join("phink").join("corpus")
371 );
372
373 let lastseed = output.join("phink").join("logs").join(LAST_SEED_FILENAME);
374 assert_eq!(phink_files.path(PFiles::LastSeed), lastseed);
375 assert_eq!(
376 lastseed.to_str().unwrap(),
377 "/tmp/phink_output/phink/logs/last_seed.phink"
378 );
379 }
380
381 #[test]
382 fn test_save_as_toml() {
383 use tempfile::tempdir;
384
385 let config = create_test_config();
386 let temp_dir = tempdir().unwrap();
387 let file_path = temp_dir.path().join("config.toml");
388
389 assert!(config.save_as_toml(file_path.to_str().unwrap()).is_ok());
390
391 let saved_config: Configuration = Configuration::try_from(&file_path).unwrap();
392 assert_eq!(saved_config, config);
393 }
394}