phink_lib/cli/
config.rs

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    /// Number of cores to use for Ziggy
45    pub cores: Option<u8>,
46    /// Also use Hongfuzz as a fuzzer
47    pub use_honggfuzz: bool,
48    // Origin deploying and instantiating the contract
49    pub deployer_address: Option<AccountId32>,
50    // Maximum number of ink! message executed per seed
51    pub max_messages_per_exec: Option<usize>,
52    /// Output directory for the coverage report
53    pub report_path: Option<PathBuf>,
54    /// Fuzz the origin. If `false`, the fuzzer will execute each message with
55    /// the same account.
56    pub fuzz_origin: bool,
57    /// The gas limit enforced when executing the constructor
58    pub default_gas_limit: Option<Weight>,
59    /// The maximum amount of balance that can be charged from the caller to
60    /// pay for the storage consumed.
61    pub storage_deposit_limit: Option<String>,
62    /// The `value` being transferred to the new account during the contract
63    /// instantiation
64    pub instantiate_initial_value: Option<String>,
65    /// In the case where you wouldn't have any default constructor in you
66    /// smart contract, i.e `new()` (without parameters), then you would
67    /// need to specify inside the config file the `Vec<u8>` representation
68    /// of the SCALE-encoded data of your constructor. This typically
69    /// involved the four first bytes of the constructor' selector,
70    /// followed by the payload.
71    pub constructor_payload: Option<String>,
72    /// Make Phink more verbose
73    pub verbose: bool,
74    /// Path where the instrumented contract will be stored after running `phink
75    /// instrument mycontract` By default, we create a random folder in
76    /// `/tmp/ink_fuzzed_XXXX`
77    pub instrumented_contract_path: Option<InstrumentedPath>,
78    /// Path where Ziggy will drop everything (logs, corpus, etc). If `None`, it'll be
79    /// `output/` by default
80    pub fuzz_output: Option<PathBuf>,
81    /// Use the Phink UI. If set to `false`, the Ziggy native UI will be used.
82    pub show_ui: bool,
83    /// If `true`, the fuzzer will detect trapped contracts (`ContractTrapped`) as a bug. Set this
84    /// to false if you just want to catch invariants. Set this to true if you want any kind of
85    /// bugs.
86    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        // Currently, TOML & Serde don't handle parsing `u128` 🤡
238        // So we need to parse it as a `string`... to then revert it to `u128`
239        // (which is `BalanceOf<T>`)
240        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}