phink_lib/fuzzer/
fuzz.rs

1use crate::{
2    cli::{
3        ui::logger::LogWriter,
4        ziggy::ZiggyConfig,
5    },
6    contract::{
7        payload::PayloadCrafter,
8        remote::{
9            ContractSetup,
10            FullContractResponse,
11        },
12        runtime::{
13            RuntimeOrigin,
14            Timestamp,
15        },
16        selectors::database::SelectorDatabase,
17    },
18    cover::coverage::InputCoverage,
19    fuzzer::{
20        environment::EnvironmentBuilder,
21        fuzz::FuzzingMode::{
22            ExecuteOneInput,
23            Fuzz,
24        },
25        manager::CampaignManager,
26        parser::{
27            try_parse_input,
28            OneInput,
29            MIN_SEED_LEN,
30        },
31    },
32    EmptyResult,
33    ResultOf,
34};
35use anyhow::Context;
36use frame_support::__private::BasicExternalities;
37use sp_core::hexdisplay::AsBytesRef;
38use std::{
39    fs,
40    path::PathBuf,
41};
42
43pub const MAX_MESSAGES_PER_EXEC: usize = 1; // One execution contains maximum 1 message.
44
45pub enum FuzzingMode {
46    ExecuteOneInput(PathBuf),
47    Fuzz,
48}
49
50#[derive(Clone)]
51pub struct Fuzzer {
52    pub ziggy_config: ZiggyConfig,
53    pub setup: ContractSetup,
54}
55
56impl Fuzzer {
57    pub fn new(ziggy_config: anyhow::Result<ZiggyConfig>) -> ResultOf<Self> {
58        let config = ziggy_config?;
59        Ok(Self {
60            ziggy_config: config.to_owned(),
61            setup: ContractSetup::initialize_wasm(config)?,
62        })
63    }
64
65    pub fn execute_harness(self, mode: FuzzingMode) -> EmptyResult {
66        match mode {
67            Fuzz => {
68                let manager = self.clone().init_fuzzer()?;
69                ziggy::fuzz!(|data: &[u8]| {
70                    self.harness(manager.to_owned(), data);
71                });
72            }
73            ExecuteOneInput(seed_path) => {
74                let manager = self
75                    .to_owned()
76                    .init_fuzzer()
77                    .context("Couldn't grap the transcoder and the invariant manager")?;
78
79                let data = fs::read(seed_path).context("Couldn't read the seed")?;
80                self.harness(manager, data.as_bytes_ref());
81            }
82        }
83
84        Ok(())
85    }
86
87    pub fn init_fuzzer(self) -> ResultOf<CampaignManager> {
88        let contract_bridge = self.setup.clone();
89
90        let invariants = PayloadCrafter::extract_invariants(&contract_bridge.json_specs)
91            .context("No invariants found, check your contract")?;
92
93        let conf = self.ziggy_config.config();
94        let messages = PayloadCrafter::extract_all(conf.instrumented_contract().to_path_buf())
95            .context("Couldn't extract all the messages selectors")?;
96
97        let payable_messages = PayloadCrafter::extract_payables(&contract_bridge.json_specs)
98            .context("Couldn't fetch payable messages")?;
99
100        let mut database = SelectorDatabase::new();
101
102        database.add_invariants(invariants);
103        database.add_messages(messages);
104        database.add_payables(payable_messages);
105
106        let env_builder = EnvironmentBuilder::new(database.clone());
107
108        env_builder
109            .build_env(self.ziggy_config.to_owned())
110            .context("Couldn't create corpus entries and dict")?;
111
112        if conf.verbose {
113            println!(
114                "\n🚀 Now fuzzing `{}` ({})!\n",
115                &contract_bridge.path_to_specs.as_os_str().to_str().unwrap(),
116                &contract_bridge.contract_address
117            );
118        }
119
120        CampaignManager::new(database, contract_bridge.clone(), conf.to_owned())
121    }
122
123    fn execute_messages(
124        &self,
125        input: &OneInput,
126        chain: &mut BasicExternalities,
127        coverage: &mut InputCoverage,
128    ) -> Vec<FullContractResponse> {
129        let mut responses = Vec::new();
130
131        chain.execute_with(|| {
132            for message in &input.messages {
133                let transfer_value = if message.is_payable {
134                    message.value_token
135                } else {
136                    0
137                };
138
139                let result: FullContractResponse = self.setup.clone().call(
140                    &message.payload,
141                    message.origin.into(),
142                    transfer_value,
143                    self.ziggy_config.config().clone(),
144                );
145
146                coverage.add_cov(result.clone().debug_message());
147                responses.push(result);
148            }
149        });
150
151        responses
152    }
153
154    pub fn harness(&self, manager: CampaignManager, input: &[u8]) {
155        if input.len() < MIN_SEED_LEN {
156            return;
157        }
158
159        let maybe_parsed_input: Option<OneInput> = try_parse_input(input, manager.to_owned());
160
161        let parsed_input = match maybe_parsed_input {
162            None => {
163                return;
164            }
165            Some(parsed) => parsed,
166        };
167
168        let mut chain = BasicExternalities::new(self.setup.genesis.clone());
169        chain.execute_with(|| {
170            Timestamp::set(RuntimeOrigin::none(), 3000).unwrap();
171        });
172
173        let mut coverage = InputCoverage::new();
174        let all_msg_responses = self.execute_messages(&parsed_input, &mut chain, &mut coverage);
175
176        let cov = coverage.messages_coverage();
177        // If we are not in fuzzing mode, we save the coverage
178        // If you ever wish to have real-time coverage while fuzzing (and a lose
179        // of performance) Simply comment out the following line :)
180        #[cfg(not(fuzzing))]
181        {
182            parsed_input.pretty_print(all_msg_responses.clone());
183
184            println!("[🚧UPDATE] Adding to the coverage file...");
185            coverage
186                .save(&manager.config().fuzz_output.unwrap_or_default())
187                .expect("🙅 Cannot save the coverage");
188            let debug = coverage.concatened_trace();
189            println!("[🚧COVERAGE] Caught identifiers {cov:?}",);
190            println!("[🚧DEBUG TRACE] Fetched the following trace: {debug:?}\n",);
191        }
192        // We now fake the coverage
193        coverage.redirect_coverage(cov);
194
195        // If the user has `show_ui` turned on, we save the fuzzed seed to display it on the UI
196        if self.ziggy_config.config().show_ui {
197            let seeder = LogWriter::new(parsed_input.to_owned(), coverage.to_owned());
198            if LogWriter::should_save() {
199                seeder.save(self.clone().ziggy_config.fuzz_output()).expect(
200                    "\nYou should run `fuzz` at least once and have a valid `output` directory\n",
201                );
202            }
203        }
204
205        // We check the invariants at the end, as we might panic
206        chain.execute_with(|| {
207            manager.check_invariants(
208                &all_msg_responses,
209                &parsed_input,
210                manager.config().catch_trapped_contract,
211            )
212        });
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use crate::{
219        cli::{
220            config::Configuration,
221            ziggy::ZiggyConfig,
222        },
223        contract::{
224            payload::PayloadCrafter,
225            selectors::database::SelectorDatabase,
226        },
227        fuzzer::{
228            environment::EnvironmentBuilder,
229            fuzz::Fuzzer,
230            manager::CampaignManager,
231        },
232        instrumenter::path::InstrumentedPath,
233        EmptyResult,
234    };
235    use contract_transcode::ContractMessageTranscoder;
236
237    use std::{
238        fs,
239        path::{
240            Path,
241            PathBuf,
242        },
243        sync::Mutex,
244    };
245    use tempfile::tempdir;
246
247    fn create_test_config() -> ZiggyConfig {
248        let config = Configuration {
249            verbose: true,
250            cores: Some(1),
251            use_honggfuzz: false,
252            fuzz_output: Some(tempdir().unwrap().into_path()),
253            instrumented_contract_path: Some(InstrumentedPath::from("sample/dns")),
254            show_ui: false,
255            max_messages_per_exec: Some(4),
256            ..Default::default()
257        };
258        ZiggyConfig::new_with_contract(config, PathBuf::from("sample/dns")).unwrap()
259    }
260
261    #[test]
262    fn test_database_and_envbuilder() -> EmptyResult {
263        let config = create_test_config();
264        let contract_bridge = Fuzzer::new(Ok(config.clone()))?.setup;
265
266        let invariants = PayloadCrafter::extract_invariants(&contract_bridge.json_specs).unwrap();
267
268        let messages = PayloadCrafter::extract_all(config.contract_path()?.clone())?
269            .into_iter()
270            .filter(|s| !invariants.contains(s))
271            .collect();
272
273        let mut database = SelectorDatabase::new();
274        database.add_invariants(invariants);
275        database.add_messages(messages);
276
277        let manager = CampaignManager::new(
278            database.clone(),
279            contract_bridge.clone(),
280            config.config().to_owned(),
281        )?;
282
283        let env_builder = EnvironmentBuilder::new(database);
284
285        env_builder.build_env(config.clone())?;
286
287        let x = manager.database();
288        let get_unique_messages = x.clone().get_unique_messages()?.len();
289
290        assert_eq!(
291            fs::read_dir(config.clone().fuzz_output().join("phink").join("corpus"))
292                .expect("Failed to read directory")
293                .count(),
294            get_unique_messages
295        );
296        assert_eq!(get_unique_messages, 5 + 1); // msg + constructor
297
298        let inv_counter = x.clone().invariants()?.len();
299        assert_eq!(inv_counter, 1);
300
301        assert_eq!(x.clone().messages()?.len(), get_unique_messages);
302
303        let dict_path = config.fuzz_output().join("phink").join("selectors.dict");
304        let dict: String = fs::read_to_string(dict_path.clone())?;
305        assert!(dict.contains("********"));
306        assert!(dict.contains("# Dictionary file for selector"));
307
308        Ok(())
309    }
310    #[test]
311    fn test_decode_constructor() {
312        let metadata_path =
313            Path::new("sample/multi-contract-caller/target/ink/multi_contract_caller.json");
314        let transcoder = Mutex::new(
315            ContractMessageTranscoder::load(metadata_path)
316                .expect("Failed to load `ContractMessageTranscoder`"),
317        );
318
319        let encoded_bytes =
320            hex::decode("9BAE9D5E5C1100007B000000ACAC0000CC5B763F7AA51000F4BD3F32F51151FF017FD22F9404D0308AFBDB3DE6F2E030E23910AC7DCDBB41BC52F1F2F923E49BAF32E9587DCD4D43D50408B62431D7B79C1A506DBEC4785423DDF36E66E2BEBA6CFEFCDD4F5708DFA3388E48").unwrap();
321        let result = transcoder
322            .lock()
323            .unwrap()
324            .decode_contract_constructor(&mut &encoded_bytes[..])
325            .unwrap();
326        // println!("{}", result);
327        let expected = "new { init_value: 4444, version: 123, accumulator_code_hash: 0xacac0000cc5b763f7aa51000f4bd3f32f51151ff017fd22f9404d0308afbdb3d, adder_code_hash: 0xe6f2e030e23910ac7dcdbb41bc52f1f2f923e49baf32e9587dcd4d43d50408b6, subber_code_hash: 0x2431d7b79c1a506dbec4785423ddf36e66e2beba6cfefcdd4f5708dfa3388e48 }";
328        assert_eq!(result.to_string(), expected);
329    }
330
331    #[test]
332    fn test_parse_input() {
333        let metadata_path = Path::new("sample/dns/target/ink/dns.json");
334        let transcoder = Mutex::new(
335            ContractMessageTranscoder::load(metadata_path)
336                .expect("Failed to load `ContractMessageTranscoder`"),
337        );
338
339        let encoded_bytes =
340            hex::decode("229b553f9400000000000000000027272727272727272700002727272727272727272727")
341                .expect("Failed to decode hex string");
342
343        assert!(
344            transcoder
345                .lock()
346                .unwrap()
347                .decode_contract_message(&mut &encoded_bytes[..])
348                .is_ok(),
349            "Failed to decode contract message"
350        );
351
352        let binding = transcoder.lock().unwrap();
353        let messages = binding.metadata().spec().messages();
354        assert!(!messages.is_empty(), "There should be some messages here");
355    }
356
357    #[test]
358    fn test_parse_dummy() {
359        let metadata_path = Path::new("sample/dummy/target/ink/dummy.json");
360        let transcoder = Mutex::new(
361            ContractMessageTranscoder::load(metadata_path)
362                .expect("Failed to load `ContractMessageTranscoder`"),
363        );
364
365        let encoded_bytes = hex::decode("fa80c2f600").expect("Failed to decode hex string");
366
367        assert!(
368            transcoder
369                .lock()
370                .unwrap()
371                .decode_contract_message(&mut &encoded_bytes[..])
372                .is_ok(),
373            "Failed to decode contract message"
374        );
375
376        let binding = transcoder.lock().unwrap();
377        let messages = binding.metadata().spec().messages();
378        assert!(!messages.is_empty(), "There should be some messages here");
379    }
380}