diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index eae88dcb8d..0201a4ce89 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -113,6 +113,7 @@ jobs: run: | rustup install nightly rustup override set nightly + rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt - name: Check for compilation errors in transaction fuzzer run: | cd tools/fuzzing diff --git a/ledger/src/transaction_pool.rs b/ledger/src/transaction_pool.rs index d6dea919c1..14a8ca340f 100644 --- a/ledger/src/transaction_pool.rs +++ b/ledger/src/transaction_pool.rs @@ -241,6 +241,7 @@ pub mod diff { } } + #[derive(Debug)] pub struct Diff { pub list: Vec, } diff --git a/tools/fuzzing/Cargo.toml b/tools/fuzzing/Cargo.toml index 39d7d0d674..7f1cda5eda 100644 --- a/tools/fuzzing/Cargo.toml +++ b/tools/fuzzing/Cargo.toml @@ -37,7 +37,7 @@ itertools = "0.11.0" binprot = { git = "https://github.com/openmina/binprot-rs", rev = "400b52c" } binprot_derive = { git = "https://github.com/openmina/binprot-rs", rev = "400b52c" } clap = "4.5.20" - +node = { path = "../../node" } [profile.release] opt-level = 3 diff --git a/tools/fuzzing/src/main.rs b/tools/fuzzing/src/main.rs index b8ebf02ecb..3fa13fa44e 100644 --- a/tools/fuzzing/src/main.rs +++ b/tools/fuzzing/src/main.rs @@ -8,10 +8,9 @@ pub mod transaction_fuzzer { pub mod generator; pub mod invariants; pub mod mutator; - use binprot::{ macros::{BinProtRead, BinProtWrite}, - BinProtRead, BinProtSize, BinProtWrite, + BinProtRead, BinProtSize, BinProtWrite, SmallString1k, }; use context::{ApplyTxResult, FuzzerCtx, FuzzerCtxBuilder}; use coverage::{ @@ -20,7 +19,7 @@ pub mod transaction_fuzzer { stats::Stats, }; use ledger::{ - scan_state::transaction_logic::{zkapp_command::ZkAppCommand, Transaction, UserCommand}, + scan_state::transaction_logic::{Transaction, UserCommand}, sparse_ledger::LedgerIntf, Account, BaseLedger, }; @@ -28,6 +27,7 @@ pub mod transaction_fuzzer { use mina_p2p_messages::bigint::BigInt; use openmina_core::constants::ConstraintConstantsUnversioned; use std::io::{Read, Write}; + use std::panic; use std::{ env, process::{ChildStdin, ChildStdout}, @@ -105,6 +105,11 @@ pub mod transaction_fuzzer { .filter_path(".rustup/") .filter_path("mina-p2p-messages/") .filter_path("core/") + .filter_path("tools/") + .filter_path("p2p/") + .filter_path("node/") + .filter_path("vrf/") + .filter_path("snark/") .filter_path("proofs/") ); } @@ -115,6 +120,8 @@ pub mod transaction_fuzzer { enum Action { SetConstraintConstants(ConstraintConstantsUnversioned), SetInitialAccounts(Vec), + SetupPool, + PoolVerify(UserCommand), GetAccounts, ApplyTx(UserCommand), #[allow(dead_code)] @@ -125,11 +132,39 @@ pub mod transaction_fuzzer { enum ActionOutput { ConstraintConstantsSet, InitialAccountsSet(BigInt), + SetupPool, + PoolVerify(Result, SmallString1k>), Accounts(Vec), TxApplied(ApplyTxResult), ExitAck, } + #[coverage(off)] + fn ocaml_setup_pool(stdin: &mut ChildStdin, stdout: &mut ChildStdout) { + let action = Action::SetupPool; + serialize(&action, stdin); + let output: ActionOutput = deserialize(stdout); + match output { + ActionOutput::SetupPool => (), + _ => panic!("Expected SetupPool"), + } + } + + #[coverage(off)] + fn ocaml_pool_verify( + stdin: &mut ChildStdin, + stdout: &mut ChildStdout, + user_command: UserCommand, + ) -> Result, SmallString1k> { + let action = Action::PoolVerify(user_command); + serialize(&action, stdin); + let output: ActionOutput = deserialize(stdout); + match output { + ActionOutput::PoolVerify(result) => result, + _ => panic!("Expected SetupPool"), + } + } + #[coverage(off)] fn ocaml_set_initial_accounts( ctx: &mut FuzzerCtx, @@ -197,6 +232,8 @@ pub mod transaction_fuzzer { break_on_invariant: bool, seed: u64, minimum_fee: u64, + pool_fuzzing: bool, + transaction_application_fuzzing: bool, ) { *invariants::BREAK.write().unwrap() = break_on_invariant; let mut cov_stats = CoverageStats::new(); @@ -210,6 +247,10 @@ pub mod transaction_fuzzer { ocaml_set_constraint_constants(&mut ctx, stdin, stdout); ocaml_set_initial_accounts(&mut ctx, stdin, stdout); + if pool_fuzzing { + ocaml_setup_pool(stdin, stdout); + } + let mut fuzzer_made_progress = false; for iteration in 0.. { @@ -238,86 +279,145 @@ pub mod transaction_fuzzer { } let user_command: UserCommand = ctx.random_user_command(); - let ocaml_apply_result = ocaml_apply_transaction(stdin, stdout, user_command.clone()); - let mut ledger = ctx.get_ledger_inner().make_child(); - - // Apply transaction on the Rust side - if let Err(error) = - ctx.apply_transaction(&mut ledger, &user_command, &ocaml_apply_result) - { - println!("!!! {error}"); - // Diff generated command form serialized version (detect hash inconsitencies) - if let Transaction::Command(ocaml_user_command) = - ocaml_apply_result.apply_result[0].transaction().data - { - if let UserCommand::ZkAppCommand(command) = &ocaml_user_command { - command.account_updates.ensure_hashed(); + if pool_fuzzing { + let ocaml_pool_verify_result = + ocaml_pool_verify(stdin, stdout, user_command.clone()); + + match panic::catch_unwind( + #[coverage(off)] + || ctx.pool_verify(&user_command, &ocaml_pool_verify_result), + ) { + Ok(mismatch) => { + if mismatch { + let mut ledger = ctx.get_ledger_inner().make_child(); + let bigint: num_bigint::BigUint = + LedgerIntf::merkle_root(&mut ledger).into(); + ctx.save_fuzzcase(&user_command, &bigint.to_string()); + + std::process::exit(0); + } else { + if let Err(_error) = ocaml_pool_verify_result { + //println!("Skipping application: {:?}", _error); + continue; + } + } + } + Err(_) => { + println!("!!! PANIC detected"); + let mut ledger = ctx.get_ledger_inner().make_child(); + let bigint: num_bigint::BigUint = + LedgerIntf::merkle_root(&mut ledger).into(); + ctx.save_fuzzcase(&user_command, &bigint.to_string()); + + std::process::exit(0); } - - println!("{}", ctx.diagnostic(&user_command, &ocaml_user_command)); } + } + + if transaction_application_fuzzing { + let ocaml_apply_result = + ocaml_apply_transaction(stdin, stdout, user_command.clone()); + let mut ledger = ctx.get_ledger_inner().make_child(); - let ocaml_accounts = ocaml_get_accounts(stdin, stdout); - let rust_accounts = ledger.to_list(); + // Apply transaction on the Rust side + if let Err(error) = + ctx.apply_transaction(&mut ledger, &user_command, &ocaml_apply_result) + { + println!("!!! {error}"); + + // Diff generated command form serialized version (detect hash inconsitencies) + if let Transaction::Command(ocaml_user_command) = + ocaml_apply_result.apply_result[0].transaction().data + { + if let UserCommand::ZkAppCommand(command) = &ocaml_user_command { + command.account_updates.ensure_hashed(); + } + + println!("{}", ctx.diagnostic(&user_command, &ocaml_user_command)); + } - for ocaml_account in ocaml_accounts.iter() { - match rust_accounts.iter().find( - #[coverage(off)] - |account| account.public_key == ocaml_account.public_key, - ) { - Some(rust_account) => { - if rust_account != ocaml_account { + let ocaml_accounts = ocaml_get_accounts(stdin, stdout); + let rust_accounts = ledger.to_list(); + + for ocaml_account in ocaml_accounts.iter() { + match rust_accounts.iter().find( + #[coverage(off)] + |account| account.public_key == ocaml_account.public_key, + ) { + Some(rust_account) => { + if rust_account != ocaml_account { + println!( + "Content mismatch between OCaml and Rust account:\n{}", + ctx.diagnostic(rust_account, ocaml_account) + ); + } + } + None => { println!( - "Content mismatch between OCaml and Rust account:\n{}", - ctx.diagnostic(rust_account, ocaml_account) + "OCaml account not present in Rust ledger: {:?}", + ocaml_account ); } } - None => { + } + + for rust_account in rust_accounts.iter() { + if !ocaml_accounts.iter().any( + #[coverage(off)] + |account| account.public_key == rust_account.public_key, + ) { println!( - "OCaml account not present in Rust ledger: {:?}", - ocaml_account + "Rust account not present in Ocaml ledger: {:?}", + rust_account ); } } - } - for rust_account in rust_accounts.iter() { - if !ocaml_accounts.iter().any( - #[coverage(off)] - |account| account.public_key == rust_account.public_key, - ) { - println!( - "Rust account not present in Ocaml ledger: {:?}", - rust_account - ); - } - } + let bigint: num_bigint::BigUint = LedgerIntf::merkle_root(&mut ledger).into(); + ctx.save_fuzzcase(&user_command, &bigint.to_string()); - let bigint: num_bigint::BigUint = LedgerIntf::merkle_root(&mut ledger).into(); - ctx.save_fuzzcase(&user_command, &bigint.to_string()); - - // Exiting due to inconsistent state - std::process::exit(0); + // Exiting due to inconsistent state + std::process::exit(0); + } } } } #[coverage(off)] - pub fn reproduce(stdin: &mut ChildStdin, stdout: &mut ChildStdout, fuzzcase: &String) { + pub fn reproduce( + stdin: &mut ChildStdin, + stdout: &mut ChildStdout, + fuzzcase: &String, + pool_fuzzing: bool, + transaction_application_fuzzing: bool, + ) { let mut ctx = FuzzerCtxBuilder::new().build(); let user_command = ctx.load_fuzzcase(fuzzcase); ocaml_set_constraint_constants(&mut ctx, stdin, stdout); ocaml_set_initial_accounts(&mut ctx, stdin, stdout); - let mut ledger = ctx.get_ledger_inner().make_child(); - let ocaml_apply_result = ocaml_apply_transaction(stdin, stdout, user_command.clone()); - let rust_apply_result = - ctx.apply_transaction(&mut ledger, &user_command, &ocaml_apply_result); + if pool_fuzzing { + ocaml_setup_pool(stdin, stdout); - println!("apply_transaction: {:?}", rust_apply_result); + let ocaml_pool_verify_result = ocaml_pool_verify(stdin, stdout, user_command.clone()); + + println!("OCaml pool verify: {:?}", ocaml_pool_verify_result); + + if ctx.pool_verify(&user_command, &ocaml_pool_verify_result) { + return; + } + } + + if transaction_application_fuzzing { + let mut ledger = ctx.get_ledger_inner().make_child(); + let ocaml_apply_result = ocaml_apply_transaction(stdin, stdout, user_command.clone()); + let rust_apply_result = + ctx.apply_transaction(&mut ledger, &user_command, &ocaml_apply_result); + + println!("apply_transaction: {:?}", rust_apply_result); + } } } @@ -340,6 +440,18 @@ fn main() { .default_value("42") .value_parser(clap::value_parser!(u64)), ) + .arg( + clap::Arg::new("pool-fuzzing") + .long("pool-fuzzing") + .default_value("true") + .value_parser(clap::value_parser!(bool)), + ) + .arg( + clap::Arg::new("transaction-application-fuzzing") + .long("transaction-application-fuzzing") + .default_value("true") + .value_parser(clap::value_parser!(bool)), + ) .get_matches(); let mut child = Command::new( @@ -363,16 +475,33 @@ fn main() { let stdin = child.stdin.as_mut().expect("Failed to open stdin"); let stdout = child.stdout.as_mut().expect("Failed to open stdout"); + let pool_fuzzing = *matches.get_one::("pool-fuzzing").unwrap(); + let transaction_application_fuzzing = *matches + .get_one::("transaction-application-fuzzing") + .unwrap(); + if let Some(fuzzcase) = matches.get_one::("fuzzcase") { println!("Reproducing fuzzcase from file: {}", fuzzcase); - transaction_fuzzer::reproduce(stdin, stdout, fuzzcase); + transaction_fuzzer::reproduce( + stdin, + stdout, + fuzzcase, + pool_fuzzing, + transaction_application_fuzzing, + ); } else { - let Some(seed) = matches.get_one::("seed") else { - unreachable!() - }; - - println!("Running the fuzzer with seed {seed}..."); - transaction_fuzzer::fuzz(stdin, stdout, true, *seed, 1000); + let seed = *matches.get_one::("seed").unwrap(); + println!("Fuzzing [seed: {seed}] [transaction application: {transaction_application_fuzzing} ] [pool: {pool_fuzzing}]..."); + + transaction_fuzzer::fuzz( + stdin, + stdout, + true, + seed, + 1000, + pool_fuzzing, + transaction_application_fuzzing, + ); } } } diff --git a/tools/fuzzing/src/transaction_fuzzer/context.rs b/tools/fuzzing/src/transaction_fuzzer/context.rs index fa9417a9e7..0ae131484b 100644 --- a/tools/fuzzing/src/transaction_fuzzer/context.rs +++ b/tools/fuzzing/src/transaction_fuzzer/context.rs @@ -5,10 +5,6 @@ use crate::transaction_fuzzer::{ }; use ark_ff::fields::arithmetic::InvalidBigInt; use ark_ff::Zero; -use ledger::scan_state::currency::{Amount, Fee, Length, Magnitude, Nonce, Signed, Slot}; -use ledger::scan_state::transaction_logic::protocol_state::{ - protocol_state_view, EpochData, EpochLedger, ProtocolStateView, -}; use ledger::scan_state::transaction_logic::transaction_applied::{ signed_command_applied, CommandApplied, TransactionApplied, Varying, }; @@ -18,6 +14,16 @@ use ledger::scan_state::transaction_logic::{ use ledger::sparse_ledger::LedgerIntf; use ledger::staged_ledger::staged_ledger::StagedLedger; use ledger::{dummy, Account, AccountId, Database, Mask, Timing, TokenId}; +use ledger::{ + scan_state::currency::{Amount, Fee, Length, Magnitude, Nonce, Signed, Slot}, + transaction_pool::TransactionPool, +}; +use ledger::{ + scan_state::transaction_logic::protocol_state::{ + protocol_state_view, EpochData, EpochLedger, ProtocolStateView, + }, + transaction_pool, +}; use mina_curves::pasta::Fq; use mina_hasher::Fp; use mina_p2p_messages::binprot::SmallString1k; @@ -29,27 +35,14 @@ use mina_p2p_messages::{ }, }; use mina_signer::{CompressedPubKey, Keypair}; -use openmina_core::constants::ConstraintConstants; +use node::DEVNET_CONFIG; +use openmina_core::{consensus::ConsensusConstants, constants::ConstraintConstants, NetworkConfig}; use rand::{rngs::SmallRng, seq::SliceRandom, Rng, SeedableRng}; use ring_buffer::RingBuffer; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::Debug; use std::{fs, str::FromStr}; -/// Same values when we run `dune runtest src/lib/staged_ledger -f` -pub const CONSTRAINT_CONSTANTS: ConstraintConstants = ConstraintConstants { - sub_windows_per_window: 11, - ledger_depth: 35, - work_delay: 2, - block_window_duration_ms: 180000, - transaction_capacity_log_2: 7, - pending_coinbase_depth: 5, - coinbase_amount: 720000000000, - supercharged_coinbase_factor: 2, - account_creation_fee: 1000000000, - fork: None, -}; - // Taken from ocaml_tests /// Same values when we run `dune runtest src/lib/staged_ledger -f` #[coverage(off)] @@ -267,6 +260,7 @@ pub struct GeneratorCtx { pub struct FuzzerCtx { pub constraint_constants: ConstraintConstants, pub txn_state_view: ProtocolStateView, + pub pool: TransactionPool, pub fuzzcases_path: String, pub gen: GeneratorCtx, pub state: FuzzerState, @@ -349,11 +343,13 @@ impl FuzzerCtx { } #[coverage(off)] - pub fn get_account(&mut self, pkey: &CompressedPubKey) -> Option { - let account_location = LedgerIntf::location_of_account( - self.get_ledger_inner(), - &AccountId::new(pkey.clone(), TokenId::default()), - ); + pub fn get_account(&self, pkey: &CompressedPubKey) -> Option { + self.get_account_by_id(&AccountId::new(pkey.clone(), TokenId::default())) + } + + #[coverage(off)] + pub fn get_account_by_id(&self, account_id: &AccountId) -> Option { + let account_location = LedgerIntf::location_of_account(self.get_ledger_inner(), account_id); account_location.map( #[coverage(off)] @@ -362,7 +358,7 @@ impl FuzzerCtx { } #[coverage(off)] - pub fn find_sender(&mut self, pkey: &CompressedPubKey) -> Option<&(Keypair, PermissionModel)> { + pub fn find_sender(&self, pkey: &CompressedPubKey) -> Option<&(Keypair, PermissionModel)> { self.state.potential_senders.iter().find( #[coverage(off)] |(kp, _)| kp.public.into_compressed() == *pkey, @@ -370,7 +366,7 @@ impl FuzzerCtx { } #[coverage(off)] - pub fn find_permissions(&mut self, pkey: &CompressedPubKey) -> Option<&PermissionModel> { + pub fn find_permissions(&self, pkey: &CompressedPubKey) -> Option<&PermissionModel> { self.find_sender(pkey).map( #[coverage(off)] |(_, pm)| pm, @@ -378,7 +374,7 @@ impl FuzzerCtx { } #[coverage(off)] - pub fn find_keypair(&mut self, pkey: &CompressedPubKey) -> Option<&Keypair> { + pub fn find_keypair(&self, pkey: &CompressedPubKey) -> Option<&Keypair> { self.find_sender(pkey).map( #[coverage(off)] |(kp, _)| kp, @@ -541,6 +537,78 @@ impl FuzzerCtx { ret } + #[coverage(off)] + pub fn pool_verify( + &self, + user_command: &UserCommand, + ocaml_pool_verify_result: &Result, SmallString1k>, + ) -> bool { + let diff = transaction_pool::diff::Diff { + list: vec![user_command.clone()], + }; + let account_ids = user_command.accounts_referenced(); + let accounts = account_ids + .iter() + .filter_map( + #[coverage(off)] + |account_id| { + self.get_account_by_id(account_id).and_then( + #[coverage(off)] + |account| Some((account_id.clone(), account)), + ) + }, + ) + .collect::>(); + + let rust_pool_result = self.pool.prevalidate(diff); + let mismatch; + + if let Ok(diff) = rust_pool_result { + let convert_diff_result = self.pool.convert_diff_to_verifiable(diff, &accounts); + + if let Ok(commands) = convert_diff_result { + let verify_result = &ledger::verifier::Verifier.verify_commands(commands, None)[0]; + let ocaml_pool_verify_result = ocaml_pool_verify_result.clone().map( + #[coverage(off)] + |commands| commands[0].clone(), + ); + + *ledger::GLOBAL_SKIP_PARTIAL_EQ.write().unwrap() = true; + mismatch = ocaml_pool_verify_result.is_ok() + && (verify_result.is_err() + || verify_result.as_ref().unwrap().forget_check() + != ocaml_pool_verify_result.clone().unwrap()); + + if mismatch { + println!( + "verify_commands: Mismatch between Rust and OCaml pool_verify_result\n{}", + self.diagnostic(&verify_result, &ocaml_pool_verify_result) + ); + } + } else { + mismatch = ocaml_pool_verify_result.is_ok(); + + if mismatch { + println!( + "convert_diff_to_verifiable: Mismatch between Rust and OCaml pool_verify_result\n{}", + self.diagnostic(&convert_diff_result, &ocaml_pool_verify_result) + ); + } + } + } else { + mismatch = ocaml_pool_verify_result.is_ok(); + + if mismatch { + println!( + "prevalidate: Mismatch between Rust and OCaml pool_verify_result\n{}", + self.diagnostic(&rust_pool_result, &ocaml_pool_verify_result) + ); + } + } + + return mismatch; + } + #[coverage(off)] pub fn apply_transaction( &mut self, @@ -694,6 +762,7 @@ impl FuzzerCtx { pub struct FuzzerCtxBuilder { constraint_constants: Option, txn_state_view: Option, + pool: Option, fuzzcases_path: Option, seed: u64, minimum_fee: u64, @@ -710,6 +779,7 @@ impl Default for FuzzerCtxBuilder { Self { constraint_constants: None, txn_state_view: None, + pool: None, fuzzcases_path: None, seed: 0, minimum_fee: 1_000_000, @@ -740,6 +810,11 @@ impl FuzzerCtxBuilder { self } + pub fn transaction_pool(&mut self, pool: TransactionPool) -> &mut Self { + self.pool = Some(pool); + self + } + #[coverage(off)] pub fn fuzzcases_path(&mut self, fuzzcases_path: String) -> &mut Self { self.fuzzcases_path = Some(fuzzcases_path); @@ -786,16 +861,35 @@ impl FuzzerCtxBuilder { #[coverage(off)] pub fn build(&mut self) -> FuzzerCtx { - let constraint_constants = self + let mut constraint_constants = self .constraint_constants .clone() - .unwrap_or(CONSTRAINT_CONSTANTS); + .unwrap_or(NetworkConfig::global().constraint_constants.clone()); + + // HACK (binprot breaks in the OCaml side) + constraint_constants.fork = None; + let depth = constraint_constants.ledger_depth as usize; let root = Mask::new_root(Database::create(depth.try_into().unwrap())); let txn_state_view = self .txn_state_view .clone() .unwrap_or(dummy_state_view(None)); + + let protocol_constants = DEVNET_CONFIG + .protocol_constants() + .expect("wrong protocol constants"); + + let default_pool = TransactionPool::new( + ledger::transaction_pool::Config { + trust_system: (), + pool_max_size: 3000, + slot_tx_end: None, + }, + &ConsensusConstants::create(&constraint_constants, &protocol_constants), + ); + + let pool = self.pool.clone().unwrap_or(default_pool); let fuzzcases_path = self.fuzzcases_path.clone().unwrap_or("./".to_string()); let ledger = match self.is_staged_ledger { @@ -813,6 +907,7 @@ impl FuzzerCtxBuilder { let mut ctx = FuzzerCtx { constraint_constants, txn_state_view, + pool, fuzzcases_path, gen: GeneratorCtx { rng: SmallRng::seed_from_u64(self.seed),