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 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 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!("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/") .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 ocaml_set_initial_accounts(&mut ctx, stdin, stdout);
268 }
269 }
270
271 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 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 if let Err(error) =
325 ctx.apply_transaction(&mut ledger, &user_command, &ocaml_apply_result)
326 {
327 println!("!!! {error}");
328
329 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 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}