transaction_fuzzer/
main.rs

1#![cfg_attr(feature = "nightly", feature(coverage_attribute))]
2#![cfg_attr(feature = "nightly", feature(stmt_expr_attributes))]
3
4#[cfg(feature = "nightly")]
5pub mod transaction_fuzzer {
6    pub mod context;
7    pub mod coverage;
8    pub mod generator;
9    pub mod invariants;
10    pub mod mutator;
11    use binprot::{
12        macros::{BinProtRead, BinProtWrite},
13        BinProtRead, BinProtSize, BinProtWrite, SmallString1k,
14    };
15    use context::{ApplyTxResult, FuzzerCtx, FuzzerCtxBuilder};
16    use coverage::{
17        cov::{Cov, FileCounters},
18        reports::CoverageReport,
19        stats::Stats,
20    };
21    use ledger::{
22        scan_state::transaction_logic::{Transaction, UserCommand},
23        sparse_ledger::LedgerIntf,
24        Account, BaseLedger,
25    };
26    use mina_curves::pasta::Fp;
27    use mina_p2p_messages::bigint::BigInt;
28    use openmina_core::constants::ConstraintConstantsUnversioned;
29    use std::{
30        env,
31        io::{Read, Write},
32        panic,
33        process::{ChildStdin, ChildStdout},
34    };
35
36    #[coverage(off)]
37    pub fn deserialize<T: BinProtRead, R: Read + ?Sized>(r: &mut R) -> T {
38        let mut prefix_buf = [0u8; 4];
39        r.read_exact(&mut prefix_buf).unwrap();
40        // The OCaml process sends a len header for the binprot data, it seems we don't really need it but we must read it.
41        let _prefix_len = u32::from_be_bytes(prefix_buf);
42        T::binprot_read(r).unwrap()
43    }
44
45    #[coverage(off)]
46    pub fn serialize<T: BinProtWrite, W: Write>(obj: &T, w: &mut W) {
47        let size = obj.binprot_size() as u32;
48        let prefix_buf: [u8; 4] = size.to_be_bytes();
49        // The OCaml process expects a len header before the binprot data.
50        w.write_all(prefix_buf.as_slice()).unwrap();
51        obj.binprot_write(w).unwrap();
52        w.flush().unwrap();
53    }
54
55    pub struct CoverageStats {
56        cov: Cov,
57        file_counters: Vec<FileCounters>,
58        pub rust: Option<Stats>,
59    }
60
61    impl Default for CoverageStats {
62        #[coverage(off)]
63        fn default() -> Self {
64            let mut cov = Cov::new();
65            let file_counters = cov.get_file_counters();
66            Self {
67                cov,
68                file_counters,
69                rust: None,
70            }
71        }
72    }
73
74    impl CoverageStats {
75        #[coverage(off)]
76        pub fn new() -> Self {
77            Self::default()
78        }
79
80        #[coverage(off)]
81        pub fn update_rust(&mut self) -> bool {
82            let rust_cov_stats = Stats::from_file_counters(&self.file_counters);
83            let coverage_increased = self.rust.is_none()
84                || rust_cov_stats.has_coverage_increased(self.rust.as_ref().unwrap());
85
86            if coverage_increased {
87                let llvm_dump = self.cov.dump();
88                let report_rust = CoverageReport::from_llvm_dump(&llvm_dump);
89                //println!("{}", report_rust);
90                println!("Saving coverage report (Rust)");
91                report_rust.write_files("rust".to_string());
92            }
93
94            self.rust = Some(rust_cov_stats);
95            coverage_increased
96        }
97
98        #[coverage(off)]
99        pub fn print(&self) {
100            if let Some(stats) = &self.rust {
101                println!(
102                    "=== COV Rust ===\n{}",
103                    stats
104                        .filter_path(".cargo/") // unwanted files
105                        .filter_path(".rustup/")
106                        .filter_path("mina-p2p-messages/")
107                        .filter_path("core/")
108                        .filter_path("tools/")
109                        .filter_path("p2p/")
110                        .filter_path("node/")
111                        .filter_path("vrf/")
112                        .filter_path("snark/")
113                        .filter_path("proofs/")
114                );
115            }
116        }
117    }
118
119    #[derive(BinProtWrite, Debug)]
120    enum Action {
121        SetConstraintConstants(ConstraintConstantsUnversioned),
122        SetInitialAccounts(Vec<Account>),
123        SetupPool,
124        PoolVerify(UserCommand),
125        GetAccounts,
126        ApplyTx(UserCommand),
127        #[allow(dead_code)]
128        Exit,
129    }
130
131    #[derive(BinProtRead, Debug)]
132    enum ActionOutput {
133        ConstraintConstantsSet,
134        InitialAccountsSet(BigInt),
135        SetupPool,
136        PoolVerify(Result<Vec<UserCommand>, SmallString1k>),
137        Accounts(Vec<Account>),
138        TxApplied(ApplyTxResult),
139        ExitAck,
140    }
141
142    #[coverage(off)]
143    fn ocaml_setup_pool(stdin: &mut ChildStdin, stdout: &mut ChildStdout) {
144        let action = Action::SetupPool;
145        serialize(&action, stdin);
146        let output: ActionOutput = deserialize(stdout);
147        match output {
148            ActionOutput::SetupPool => (),
149            _ => panic!("Expected SetupPool"),
150        }
151    }
152
153    #[coverage(off)]
154    fn ocaml_pool_verify(
155        stdin: &mut ChildStdin,
156        stdout: &mut ChildStdout,
157        user_command: UserCommand,
158    ) -> Result<Vec<UserCommand>, SmallString1k> {
159        let action = Action::PoolVerify(user_command);
160        serialize(&action, stdin);
161        let output: ActionOutput = deserialize(stdout);
162        match output {
163            ActionOutput::PoolVerify(result) => result,
164            _ => panic!("Expected SetupPool"),
165        }
166    }
167
168    #[coverage(off)]
169    fn ocaml_set_initial_accounts(
170        ctx: &mut FuzzerCtx,
171        stdin: &mut ChildStdin,
172        stdout: &mut ChildStdout,
173    ) -> Fp {
174        let action = Action::SetInitialAccounts(ctx.get_ledger_accounts());
175        serialize(&action, stdin);
176        let output: ActionOutput = deserialize(stdout);
177        let ocaml_ledger_root_hash = match output {
178            ActionOutput::InitialAccountsSet(root_hash) => root_hash,
179            _ => panic!("Expected InitialAccountsSet"),
180        };
181        let rust_ledger_root_hash = ctx.get_ledger_root();
182        assert!(ocaml_ledger_root_hash == rust_ledger_root_hash.into());
183        rust_ledger_root_hash
184    }
185
186    #[coverage(off)]
187    fn ocaml_get_accounts(stdin: &mut ChildStdin, stdout: &mut ChildStdout) -> Vec<Account> {
188        let action = Action::GetAccounts;
189        serialize(&action, stdin);
190        let output: ActionOutput = deserialize(stdout);
191
192        match output {
193            ActionOutput::Accounts(accounts) => accounts,
194            _ => unreachable!(),
195        }
196    }
197
198    #[coverage(off)]
199    fn ocaml_apply_transaction(
200        stdin: &mut ChildStdin,
201        stdout: &mut ChildStdout,
202        user_command: UserCommand,
203    ) -> ApplyTxResult {
204        let action = Action::ApplyTx(user_command);
205        serialize(&action, stdin);
206        let output: ActionOutput = deserialize(stdout);
207        match output {
208            ActionOutput::TxApplied(result) => result,
209            _ => panic!("Expected TxApplied"),
210        }
211    }
212
213    #[coverage(off)]
214    fn ocaml_set_constraint_constants(
215        ctx: &mut FuzzerCtx,
216        stdin: &mut ChildStdin,
217        stdout: &mut ChildStdout,
218    ) {
219        let action = Action::SetConstraintConstants((&ctx.constraint_constants).into());
220        serialize(&action, stdin);
221        let output: ActionOutput = deserialize(stdout);
222        match output {
223            ActionOutput::ConstraintConstantsSet => (),
224            _ => panic!("Expected ConstraintConstantsSet"),
225        }
226    }
227
228    #[coverage(off)]
229    pub fn fuzz(
230        stdin: &mut ChildStdin,
231        stdout: &mut ChildStdout,
232        break_on_invariant: bool,
233        seed: u64,
234        minimum_fee: u64,
235        pool_fuzzing: bool,
236        transaction_application_fuzzing: bool,
237    ) {
238        *invariants::BREAK.write().unwrap() = break_on_invariant;
239        let mut cov_stats = CoverageStats::new();
240        let mut ctx = FuzzerCtxBuilder::new()
241            .seed(seed)
242            .minimum_fee(minimum_fee)
243            .initial_accounts(1000)
244            .fuzzcases_path(env::var("FUZZCASES_PATH").unwrap_or("/tmp/".to_string()))
245            .build();
246
247        ocaml_set_constraint_constants(&mut ctx, stdin, stdout);
248        ocaml_set_initial_accounts(&mut ctx, stdin, stdout);
249
250        if pool_fuzzing {
251            ocaml_setup_pool(stdin, stdout);
252        }
253
254        let mut fuzzer_made_progress = false;
255
256        for iteration in 0.. {
257            print!("Iteration {}\r", iteration);
258            std::io::stdout().flush().unwrap();
259
260            if (iteration % 10000) == 0 {
261                if fuzzer_made_progress {
262                    fuzzer_made_progress = false;
263                    ctx.take_snapshot();
264                } else {
265                    ctx.restore_snapshot();
266                    // Restore ledger in OCaml
267                    ocaml_set_initial_accounts(&mut ctx, stdin, stdout);
268                }
269            }
270
271            // Update coverage statistics every 1000 iterations
272            if (iteration % 1000) == 0 {
273                let update_rust_increased_coverage = cov_stats.update_rust();
274
275                if update_rust_increased_coverage {
276                    fuzzer_made_progress = true;
277                    cov_stats.print();
278                }
279            }
280
281            let user_command: UserCommand = ctx.random_user_command();
282
283            if pool_fuzzing {
284                let ocaml_pool_verify_result =
285                    ocaml_pool_verify(stdin, stdout, user_command.clone());
286
287                match panic::catch_unwind(
288                    #[coverage(off)]
289                    || ctx.pool_verify(&user_command, &ocaml_pool_verify_result),
290                ) {
291                    Ok(mismatch) => {
292                        if mismatch {
293                            let mut ledger = ctx.get_ledger_inner().make_child();
294                            let bigint: num_bigint::BigUint =
295                                LedgerIntf::merkle_root(&mut ledger).into();
296                            ctx.save_fuzzcase(&user_command, &bigint.to_string());
297
298                            std::process::exit(0);
299                        } else {
300                            if let Err(_error) = ocaml_pool_verify_result {
301                                //println!("Skipping application: {:?}", _error);
302                                continue;
303                            }
304                        }
305                    }
306                    Err(_) => {
307                        println!("!!! PANIC detected");
308                        let mut ledger = ctx.get_ledger_inner().make_child();
309                        let bigint: num_bigint::BigUint =
310                            LedgerIntf::merkle_root(&mut ledger).into();
311                        ctx.save_fuzzcase(&user_command, &bigint.to_string());
312
313                        std::process::exit(0);
314                    }
315                }
316            }
317
318            if transaction_application_fuzzing {
319                let ocaml_apply_result =
320                    ocaml_apply_transaction(stdin, stdout, user_command.clone());
321                let mut ledger = ctx.get_ledger_inner().make_child();
322
323                // Apply transaction on the Rust side
324                if let Err(error) =
325                    ctx.apply_transaction(&mut ledger, &user_command, &ocaml_apply_result)
326                {
327                    println!("!!! {error}");
328
329                    // Diff generated command form serialized version (detect hash inconsitencies)
330                    if let Transaction::Command(ocaml_user_command) =
331                        ocaml_apply_result.apply_result[0].transaction().data
332                    {
333                        if let UserCommand::ZkAppCommand(command) = &ocaml_user_command {
334                            command.account_updates.ensure_hashed();
335                        }
336
337                        println!("{}", ctx.diagnostic(&user_command, &ocaml_user_command));
338                    }
339
340                    let ocaml_accounts = ocaml_get_accounts(stdin, stdout);
341                    let rust_accounts = ledger.to_list();
342
343                    for ocaml_account in ocaml_accounts.iter() {
344                        match rust_accounts.iter().find(
345                            #[coverage(off)]
346                            |account| account.public_key == ocaml_account.public_key,
347                        ) {
348                            Some(rust_account) => {
349                                if rust_account != ocaml_account {
350                                    println!(
351                                        "Content mismatch between OCaml and Rust account:\n{}",
352                                        ctx.diagnostic(rust_account, ocaml_account)
353                                    );
354                                }
355                            }
356                            None => {
357                                println!(
358                                    "OCaml account not present in Rust ledger: {:?}",
359                                    ocaml_account
360                                );
361                            }
362                        }
363                    }
364
365                    for rust_account in rust_accounts.iter() {
366                        if !ocaml_accounts.iter().any(
367                            #[coverage(off)]
368                            |account| account.public_key == rust_account.public_key,
369                        ) {
370                            println!(
371                                "Rust account not present in Ocaml ledger: {:?}",
372                                rust_account
373                            );
374                        }
375                    }
376
377                    let bigint: num_bigint::BigUint = LedgerIntf::merkle_root(&mut ledger).into();
378                    ctx.save_fuzzcase(&user_command, &bigint.to_string());
379
380                    // Exiting due to inconsistent state
381                    std::process::exit(0);
382                }
383            }
384        }
385    }
386
387    #[coverage(off)]
388    pub fn reproduce(
389        stdin: &mut ChildStdin,
390        stdout: &mut ChildStdout,
391        fuzzcase: &String,
392        pool_fuzzing: bool,
393        transaction_application_fuzzing: bool,
394    ) {
395        let mut ctx = FuzzerCtxBuilder::new().build();
396        let user_command = ctx.load_fuzzcase(fuzzcase);
397
398        ocaml_set_constraint_constants(&mut ctx, stdin, stdout);
399        ocaml_set_initial_accounts(&mut ctx, stdin, stdout);
400
401        if pool_fuzzing {
402            ocaml_setup_pool(stdin, stdout);
403
404            let ocaml_pool_verify_result = ocaml_pool_verify(stdin, stdout, user_command.clone());
405
406            println!("OCaml pool verify: {:?}", ocaml_pool_verify_result);
407
408            if ctx.pool_verify(&user_command, &ocaml_pool_verify_result) {
409                return;
410            }
411        }
412
413        if transaction_application_fuzzing {
414            let mut ledger = ctx.get_ledger_inner().make_child();
415            let ocaml_apply_result = ocaml_apply_transaction(stdin, stdout, user_command.clone());
416            let rust_apply_result =
417                ctx.apply_transaction(&mut ledger, &user_command, &ocaml_apply_result);
418
419            println!("apply_transaction: {:?}", rust_apply_result);
420        }
421    }
422}
423
424fn main() {
425    #[cfg(feature = "nightly")]
426    {
427        use std::process::{Command, Stdio};
428
429        let matches = clap::Command::new("Transaction Fuzzer")
430            .arg(
431                clap::Arg::new("fuzzcase")
432                    .short('f')
433                    .long("fuzzcase")
434                    .value_name("FILE"),
435            )
436            .arg(
437                clap::Arg::new("seed")
438                    .short('s')
439                    .long("seed")
440                    .default_value("42")
441                    .value_parser(clap::value_parser!(u64)),
442            )
443            .arg(
444                clap::Arg::new("pool-fuzzing")
445                    .long("pool-fuzzing")
446                    .default_value("true")
447                    .value_parser(clap::value_parser!(bool)),
448            )
449            .arg(
450                clap::Arg::new("transaction-application-fuzzing")
451                    .long("transaction-application-fuzzing")
452                    .default_value("true")
453                    .value_parser(clap::value_parser!(bool)),
454            )
455            .get_matches();
456
457        let mut child = Command::new(
458            std::env::var("OCAML_TRANSACTION_FUZZER_PATH").unwrap_or_else(
459                #[coverage(off)]
460                |_| {
461                    format!(
462                        "{}/mina/_build/default/src/app/transaction_fuzzer/transaction_fuzzer.exe",
463                        std::env::var("HOME").unwrap()
464                    )
465                },
466            ),
467        )
468        .arg("execute")
469        .stdin(Stdio::piped())
470        .stdout(Stdio::piped())
471        .stderr(Stdio::inherit())
472        .spawn()
473        .expect("Failed to start OCaml process");
474
475        let stdin = child.stdin.as_mut().expect("Failed to open stdin");
476        let stdout = child.stdout.as_mut().expect("Failed to open stdout");
477
478        let pool_fuzzing = *matches.get_one::<bool>("pool-fuzzing").unwrap();
479        let transaction_application_fuzzing = *matches
480            .get_one::<bool>("transaction-application-fuzzing")
481            .unwrap();
482
483        if let Some(fuzzcase) = matches.get_one::<String>("fuzzcase") {
484            println!("Reproducing fuzzcase from file: {}", fuzzcase);
485            transaction_fuzzer::reproduce(
486                stdin,
487                stdout,
488                fuzzcase,
489                pool_fuzzing,
490                transaction_application_fuzzing,
491            );
492        } else {
493            let seed = *matches.get_one::<u64>("seed").unwrap();
494            println!("Fuzzing [seed: {seed}] [transaction application: {transaction_application_fuzzing} ] [pool: {pool_fuzzing}]...");
495
496            transaction_fuzzer::fuzz(
497                stdin,
498                stdout,
499                true,
500                seed,
501                1000,
502                pool_fuzzing,
503                transaction_application_fuzzing,
504            );
505        }
506    }
507}