From aa5b5630a1cf456f972f1790f2e0b4a8e837f9cb Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 08:11:37 +0800 Subject: [PATCH 01/13] init fuzz harness crate --- Cargo.lock | 4 ++++ Cargo.toml | 2 ++ fuzz/Cargo.toml | 11 +++++++++++ fuzz/src/lib.rs | 6 ++++++ 4 files changed, 23 insertions(+) create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 731ec21..8eeaa04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1521,6 +1521,10 @@ dependencies = [ "solana-sdk", ] +[[package]] +name = "mollusk-svm-fuzz" +version = "0.0.4" + [[package]] name = "num" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index fdea035..70b4a6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "bencher", + "fuzz", "harness", "test-programs/cpi-target", "test-programs/primary", @@ -19,6 +20,7 @@ version = "0.0.4" bincode = "1.3.3" mollusk-svm = { path = "harness", version = "0.0.4" } mollusk-svm-bencher = { path = "bencher", version = "0.0.4" } +mollusk-svm-fuzz = { path = "fuzz", version = "0.0.4" } num-format = "0.4.4" serde_json = "1.0.117" solana-bpf-loader-program = "2.0" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..23e7ba4 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "mollusk-svm-fuzz" +description = "SVM program fuzz harness." +documentation = "https://docs.rs/mollusk-svm-fuzz" +authors = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +version = { workspace = true } + +[dependencies] diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs new file mode 100644 index 0000000..caae146 --- /dev/null +++ b/fuzz/src/lib.rs @@ -0,0 +1,6 @@ +//! Mollusk SVM Fuzz: Fuzzing harness for SVM programs. +//! +//! Note, although the fuzz harness provides an easy way to fuzz programs with +//! Mollusk, it is not required. Developers can use this fuzz harness on their +//! own custom SVM entrypoint. Hence the distinction between fixture types and +//! Mollusk types. From 3e48dd6835e8cfbe1b2d99b8b3b1d3397477b43c Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 08:14:01 +0800 Subject: [PATCH 02/13] add protos --- Cargo.lock | 170 ++++++++++++++++++++++++++++++++ Cargo.toml | 1 + fuzz/Cargo.toml | 3 + fuzz/build.rs | 19 ++++ fuzz/proto/compute_budget.proto | 50 ++++++++++ fuzz/proto/invoke.proto | 77 +++++++++++++++ fuzz/proto/sysvars.proto | 72 ++++++++++++++ 7 files changed, 392 insertions(+) create mode 100644 fuzz/build.rs create mode 100644 fuzz/proto/compute_budget.proto create mode 100644 fuzz/proto/invoke.proto create mode 100644 fuzz/proto/sysvars.proto diff --git a/Cargo.lock b/Cargo.lock index 8eeaa04..57b4445 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,12 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + [[package]] name = "ark-bn254" version = "0.4.0" @@ -572,6 +578,15 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + [[package]] name = "combine" version = "3.8.1" @@ -922,12 +937,34 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + [[package]] name = "feature-probe" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.33" @@ -1118,6 +1155,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1169,6 +1212,15 @@ dependencies = [ "hmac 0.8.1", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -1427,6 +1479,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.12" @@ -1524,6 +1582,15 @@ dependencies = [ [[package]] name = "mollusk-svm-fuzz" version = "0.0.4" +dependencies = [ + "prost-build", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "num" @@ -1733,6 +1800,16 @@ dependencies = [ "num", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1838,6 +1915,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae5a4388762d5815a9fc0dea33c56b021cdc8dde0c55e0c9ca57197254b0cab" +dependencies = [ + "bytes", + "cfg-if", + "cmake", + "heck", + "itertools 0.10.5", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" +dependencies = [ + "bytes", + "prost", +] + [[package]] name = "qstring" version = "0.7.2" @@ -2058,6 +2190,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.21.12" @@ -2671,6 +2816,19 @@ dependencies = [ "libc", ] +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -3023,6 +3181,18 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 70b4a6e..a072ad2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ mollusk-svm = { path = "harness", version = "0.0.4" } mollusk-svm-bencher = { path = "bencher", version = "0.0.4" } mollusk-svm-fuzz = { path = "fuzz", version = "0.0.4" } num-format = "0.4.4" +prost-build = "0.10" serde_json = "1.0.117" solana-bpf-loader-program = "2.0" solana-compute-budget = "2.0" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 23e7ba4..4ce4f8c 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,3 +9,6 @@ edition = { workspace = true } version = { workspace = true } [dependencies] + +[build-dependencies] +prost-build = { workspace = true } diff --git a/fuzz/build.rs b/fuzz/build.rs new file mode 100644 index 0000000..3d48a8f --- /dev/null +++ b/fuzz/build.rs @@ -0,0 +1,19 @@ +use std::io::Result; + +fn main() -> Result<()> { + let proto_base_path = std::path::PathBuf::from("proto"); + + let protos = &[ + proto_base_path.join("compute_budget.proto"), + proto_base_path.join("sysvars.proto"), + proto_base_path.join("invoke.proto"), + ]; + + protos + .iter() + .for_each(|proto| println!("cargo:rerun-if-changed={}", proto.display())); + + prost_build::compile_protos(protos, &[proto_base_path])?; + + Ok(()) +} diff --git a/fuzz/proto/compute_budget.proto b/fuzz/proto/compute_budget.proto new file mode 100644 index 0000000..45969d9 --- /dev/null +++ b/fuzz/proto/compute_budget.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; +package org.mollusk.svm; + +// Compute budget for instructions. +message ComputeBudget { + uint64 compute_unit_limit = 1; + uint64 log_64_units = 2; + uint64 create_program_address_units = 3; + uint64 invoke_units = 4; + uint64 max_instruction_stack_depth = 5; + uint64 max_instruction_trace_length = 6; + uint64 sha256_base_cost = 7; + uint64 sha256_byte_cost = 8; + uint64 sha256_max_slices = 9; + uint64 max_call_depth = 10; + uint64 stack_frame_size = 11; + uint64 log_pubkey_units = 12; + uint64 max_cpi_instruction_size = 13; + uint64 cpi_bytes_per_unit = 14; + uint64 sysvar_base_cost = 15; + uint64 secp256k1_recover_cost = 16; + uint64 syscall_base_cost = 17; + uint64 curve25519_edwards_validate_point_cost = 18; + uint64 curve25519_edwards_add_cost = 19; + uint64 curve25519_edwards_subtract_cost = 20; + uint64 curve25519_edwards_multiply_cost = 21; + uint64 curve25519_edwards_msm_base_cost = 22; + uint64 curve25519_edwards_msm_incremental_cost = 23; + uint64 curve25519_ristretto_validate_point_cost = 24; + uint64 curve25519_ristretto_add_cost = 25; + uint64 curve25519_ristretto_subtract_cost = 26; + uint64 curve25519_ristretto_multiply_cost = 27; + uint64 curve25519_ristretto_msm_base_cost = 28; + uint64 curve25519_ristretto_msm_incremental_cost = 29; + uint32 heap_size = 30; + uint64 heap_cost = 31; + uint64 mem_op_base_cost = 32; + uint64 alt_bn128_addition_cost = 33; + uint64 alt_bn128_multiplication_cost = 34; + uint64 alt_bn128_pairing_one_pair_cost_first = 35; + uint64 alt_bn128_pairing_one_pair_cost_other = 36; + uint64 big_modular_exponentiation_cost = 37; + uint64 poseidon_cost_coefficient_a = 38; + uint64 poseidon_cost_coefficient_c = 39; + uint64 get_remaining_compute_units_cost = 40; + uint64 alt_bn128_g1_compress = 41; + uint64 alt_bn128_g1_decompress = 42; + uint64 alt_bn128_g2_compress = 43; + uint64 alt_bn128_g2_decompress = 44; +} diff --git a/fuzz/proto/invoke.proto b/fuzz/proto/invoke.proto new file mode 100644 index 0000000..0141093 --- /dev/null +++ b/fuzz/proto/invoke.proto @@ -0,0 +1,77 @@ +syntax = "proto3"; +package org.mollusk.svm; + +import "compute_budget.proto"; +import "sysvars.proto"; + +// A set of feature flags. +message FeatureSet { + // Every item in this list marks an enabled feature. The value of + // each item is the first 8 bytes of the feature ID as a little- + // endian integer. + repeated fixed64 features = 1; +} + +// The complete state of an account excluding its public key. +message AcctState { + // The account key. Can be omitted if obvious from the context. + bytes address = 1; + + uint64 lamports = 2; + bytes data = 3; + bool executable = 4; + uint64 rent_epoch = 5; + bytes owner = 6; +} + +message InstrAcct { + // Selects an account in an external list + uint32 index = 1; + bool is_signer = 2; + bool is_writable = 3; +} + +// The execution context of an instruction. Contains all required +// information to independently replay an instruction. +message InstrContext { + ComputeBudget compute_budget = 1; + + FeatureSet feature_set = 2; + + SysvarContext sysvars = 3; + + // The program invoked. + bytes program_id = 4; + + // Account access list for this instruction (refers to above accounts list) + repeated InstrAcct instr_accounts = 5; + + // The input data passed to program execution. + bytes data = 6; + + // Account state accessed by the instruction. + repeated AcctState accounts = 7; +} + +// The results of executing an InstrContext. +message InstrEffects { + // Compute units consumed by the instruction. + uint64 compute_units_consumed = 1; + + // Execution time for instruction. + uint64 execution_time = 2; + + // Program return code. Zero is success, errors are non-zero. + uint32 program_result = 3; + + // Copies of accounts that were provided to the instruction. May be in an + // arbitrary order. The pubkey of each account is unique in this list. Each + // account address must also be in the InstrContext. + repeated AcctState resulting_accounts = 4; +} + +// An instruction processing test fixture. +message InstrFixture { + InstrContext input = 1; + InstrEffects output = 2; +} diff --git a/fuzz/proto/sysvars.proto b/fuzz/proto/sysvars.proto new file mode 100644 index 0000000..19568ad --- /dev/null +++ b/fuzz/proto/sysvars.proto @@ -0,0 +1,72 @@ +syntax = "proto3"; +package org.mollusk.svm; + +// The `Clock` sysvar. +message Clock { + uint64 slot = 1; + int64 epoch_start_timestamp = 2; + uint64 epoch = 3; + uint64 leader_schedule_epoch = 4; + int64 unix_timestamp = 5; +} + +// The `EpochRewards` sysvar. +message EpochRewards { + uint64 distribution_starting_block_height = 1; + uint64 num_partitions = 2; + bytes parent_blockhash = 3; + bytes total_points = 4; + uint64 total_rewards = 5; + uint64 distributed_rewards = 6; + bool active = 7; +} + +// The `EpochSchedule` sysvar. +message EpochSchedule { + uint64 slots_per_epoch = 1; + uint64 leader_schedule_slot_offset = 2; + bool warmup = 3; + uint64 first_normal_epoch = 4; + uint64 first_normal_slot = 5; +} + +// The `Rent` sysvar. +message Rent { + uint64 lamports_per_byte_year = 1; + double exemption_threshold = 2; + uint32 burn_percent = 3; +} + +// A `SlotHashEntry` entry for the `SlotHashes` sysvar. +message SlotHashEntry { + uint64 slot = 1; + bytes hash = 2; +} + +// The `SlotHashes` sysvar. +message SlotHashes { + repeated SlotHashEntry slot_hashes = 1; +} + +// A `StakeHistoryEntry` entry for the `StakeHistory` sysvar. +message StakeHistoryEntry { + uint64 epoch = 1; + uint64 effective = 2; + uint64 activating = 3; + uint64 deactivating = 4; +} + +// The `StakeHistory` sysvar. +message StakeHistory { + repeated StakeHistoryEntry stake_history = 1; +} + +// The sysvar context. +message SysvarContext { + Clock clock = 1; + EpochRewards epoch_rewards = 2; + EpochSchedule epoch_schedule = 3; + Rent rent = 4; + SlotHashes slot_hashes = 5; + StakeHistory stake_history = 6; +} From 4de26c83ae32f817c5638e8cb8d4520cbb9e278b Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 14:57:00 +0800 Subject: [PATCH 03/13] add fixtures --- Cargo.lock | 5 + Cargo.toml | 2 + fuzz/Cargo.toml | 5 + fuzz/src/fixture/account.rs | 113 +++++++++ fuzz/src/fixture/compute_budget.rs | 107 ++++++++ fuzz/src/fixture/context.rs | 95 ++++++++ fuzz/src/fixture/effects.rs | 118 +++++++++ fuzz/src/fixture/error.rs | 34 +++ fuzz/src/fixture/feature_set.rs | 90 +++++++ fuzz/src/fixture/mod.rs | 50 ++++ fuzz/src/fixture/sysvars.rs | 378 +++++++++++++++++++++++++++++ fuzz/src/lib.rs | 2 + 12 files changed, 999 insertions(+) create mode 100644 fuzz/src/fixture/account.rs create mode 100644 fuzz/src/fixture/compute_budget.rs create mode 100644 fuzz/src/fixture/context.rs create mode 100644 fuzz/src/fixture/effects.rs create mode 100644 fuzz/src/fixture/error.rs create mode 100644 fuzz/src/fixture/feature_set.rs create mode 100644 fuzz/src/fixture/mod.rs create mode 100644 fuzz/src/fixture/sysvars.rs diff --git a/Cargo.lock b/Cargo.lock index 57b4445..4dea436 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1583,7 +1583,12 @@ dependencies = [ name = "mollusk-svm-fuzz" version = "0.0.4" dependencies = [ + "prost", "prost-build", + "prost-types", + "solana-compute-budget", + "solana-sdk", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a072ad2..bc1ec4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,9 @@ mollusk-svm = { path = "harness", version = "0.0.4" } mollusk-svm-bencher = { path = "bencher", version = "0.0.4" } mollusk-svm-fuzz = { path = "fuzz", version = "0.0.4" } num-format = "0.4.4" +prost = "0.10" prost-build = "0.10" +prost-types = "0.10" serde_json = "1.0.117" solana-bpf-loader-program = "2.0" solana-compute-budget = "2.0" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 4ce4f8c..011a0c5 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,6 +9,11 @@ edition = { workspace = true } version = { workspace = true } [dependencies] +prost = { workspace = true } +prost-types = { workspace = true } +solana-compute-budget = { workspace = true } +solana-sdk = { workspace = true } +thiserror = { workspace = true } [build-dependencies] prost-build = { workspace = true } diff --git a/fuzz/src/fixture/account.rs b/fuzz/src/fixture/account.rs new file mode 100644 index 0000000..df0555e --- /dev/null +++ b/fuzz/src/fixture/account.rs @@ -0,0 +1,113 @@ +//! An account with an address, in the form of a `(Pubkey, AccountSharedData)` +//! tuple from the Solana SDK. + +use { + super::{error::FixtureError, proto}, + solana_sdk::{ + account::{Account, AccountSharedData}, + pubkey::Pubkey, + }, +}; + +impl TryFrom for (Pubkey, AccountSharedData) { + type Error = FixtureError; + + fn try_from(input: proto::AcctState) -> Result { + let proto::AcctState { + address, + owner, + lamports, + data, + executable, + rent_epoch, + } = input; + + let pubkey = Pubkey::new_from_array( + address + .try_into() + .map_err(|_| FixtureError::InvalidPubkeyBytes)?, + ); + let owner = Pubkey::new_from_array( + owner + .try_into() + .map_err(|_| FixtureError::InvalidPubkeyBytes)?, + ); + + Ok(( + pubkey, + AccountSharedData::from(Account { + lamports, + data, + owner, + executable, + rent_epoch, + }), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_try_from_proto_acct_state() { + let try_conversion = |address, owner| { + let input = proto::AcctState { + address, + owner, + lamports: 42, + data: vec![1, 2, 3], + executable: true, + rent_epoch: 0, + }; + TryInto::<(Pubkey, AccountSharedData)>::try_into(input) + }; + + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + + // Success + let (result_pubkey, result_account) = + try_conversion(pubkey.to_bytes().to_vec(), owner.to_bytes().to_vec()).unwrap(); + assert_eq!(result_pubkey, pubkey); + assert_eq!( + result_account, + AccountSharedData::from(Account { + lamports: 42, + data: vec![1, 2, 3], + owner, + executable: true, + rent_epoch: 0, + }) + ); + + // Failures + let too_many_bytes = vec![0; 33]; + let too_few_bytes = vec![0; 31]; + + // Too many bytes for address + assert_eq!( + try_conversion(too_many_bytes.clone(), owner.to_bytes().to_vec()).unwrap_err(), + FixtureError::InvalidPubkeyBytes + ); + + // Too few bytes for address + assert_eq!( + try_conversion(too_few_bytes.clone(), owner.to_bytes().to_vec()).unwrap_err(), + FixtureError::InvalidPubkeyBytes + ); + + // Too many bytes for owner + assert_eq!( + try_conversion(pubkey.to_bytes().to_vec(), too_many_bytes).unwrap_err(), + FixtureError::InvalidPubkeyBytes + ); + + // Too few bytes for owner + assert_eq!( + try_conversion(pubkey.to_bytes().to_vec(), too_few_bytes).unwrap_err(), + FixtureError::InvalidPubkeyBytes + ); + } +} diff --git a/fuzz/src/fixture/compute_budget.rs b/fuzz/src/fixture/compute_budget.rs new file mode 100644 index 0000000..346fe4e --- /dev/null +++ b/fuzz/src/fixture/compute_budget.rs @@ -0,0 +1,107 @@ +//! Compute budget for instructions. + +use {super::proto, solana_compute_budget::compute_budget::ComputeBudget}; + +impl From for ComputeBudget { + fn from(input: proto::ComputeBudget) -> Self { + let proto::ComputeBudget { + compute_unit_limit, + log_64_units, + create_program_address_units, + invoke_units, + max_instruction_stack_depth, + max_instruction_trace_length, + sha256_base_cost, + sha256_byte_cost, + sha256_max_slices, + max_call_depth, + stack_frame_size, + log_pubkey_units, + max_cpi_instruction_size, + cpi_bytes_per_unit, + sysvar_base_cost, + secp256k1_recover_cost, + syscall_base_cost, + curve25519_edwards_validate_point_cost, + curve25519_edwards_add_cost, + curve25519_edwards_subtract_cost, + curve25519_edwards_multiply_cost, + curve25519_edwards_msm_base_cost, + curve25519_edwards_msm_incremental_cost, + curve25519_ristretto_validate_point_cost, + curve25519_ristretto_add_cost, + curve25519_ristretto_subtract_cost, + curve25519_ristretto_multiply_cost, + curve25519_ristretto_msm_base_cost, + curve25519_ristretto_msm_incremental_cost, + heap_size, + heap_cost, + mem_op_base_cost, + alt_bn128_addition_cost, + alt_bn128_multiplication_cost, + alt_bn128_pairing_one_pair_cost_first, + alt_bn128_pairing_one_pair_cost_other, + big_modular_exponentiation_cost, + poseidon_cost_coefficient_a, + poseidon_cost_coefficient_c, + get_remaining_compute_units_cost, + alt_bn128_g1_compress, + alt_bn128_g1_decompress, + alt_bn128_g2_compress, + alt_bn128_g2_decompress, + } = input; + + let max_instruction_stack_depth = max_instruction_stack_depth as usize; + let max_instruction_trace_length = max_instruction_trace_length as usize; + let max_call_depth = max_call_depth as usize; + let stack_frame_size = stack_frame_size as usize; + let max_cpi_instruction_size = max_cpi_instruction_size as usize; + + ComputeBudget { + compute_unit_limit, + log_64_units, + create_program_address_units, + invoke_units, + max_instruction_stack_depth, + max_instruction_trace_length, + sha256_base_cost, + sha256_byte_cost, + sha256_max_slices, + max_call_depth, + stack_frame_size, + log_pubkey_units, + max_cpi_instruction_size, + cpi_bytes_per_unit, + sysvar_base_cost, + secp256k1_recover_cost, + syscall_base_cost, + curve25519_edwards_validate_point_cost, + curve25519_edwards_add_cost, + curve25519_edwards_subtract_cost, + curve25519_edwards_multiply_cost, + curve25519_edwards_msm_base_cost, + curve25519_edwards_msm_incremental_cost, + curve25519_ristretto_validate_point_cost, + curve25519_ristretto_add_cost, + curve25519_ristretto_subtract_cost, + curve25519_ristretto_multiply_cost, + curve25519_ristretto_msm_base_cost, + curve25519_ristretto_msm_incremental_cost, + heap_size, + heap_cost, + mem_op_base_cost, + alt_bn128_addition_cost, + alt_bn128_multiplication_cost, + alt_bn128_pairing_one_pair_cost_first, + alt_bn128_pairing_one_pair_cost_other, + big_modular_exponentiation_cost, + poseidon_cost_coefficient_a, + poseidon_cost_coefficient_c, + get_remaining_compute_units_cost, + alt_bn128_g1_compress, + alt_bn128_g1_decompress, + alt_bn128_g2_compress, + alt_bn128_g2_decompress, + } + } +} diff --git a/fuzz/src/fixture/context.rs b/fuzz/src/fixture/context.rs new file mode 100644 index 0000000..c50925f --- /dev/null +++ b/fuzz/src/fixture/context.rs @@ -0,0 +1,95 @@ +//! Instruction context fixture for invoking programs in a simulated program +//! runtime environment. + +use { + super::{ + error::FixtureError, feature_set::FixtureFeatureSet, proto, sysvars::FixtureSysvarContext, + }, + solana_compute_budget::compute_budget::ComputeBudget, + solana_sdk::{account::AccountSharedData, instruction::AccountMeta, pubkey::Pubkey}, +}; + +/// Instruction context fixture. +#[derive(Debug)] +pub struct FixtureContext { + /// The compute budget to use for the simulation. + pub compute_budget: ComputeBudget, + /// The feature set to use for the simulation. + pub feature_set: FixtureFeatureSet, + /// The sysvar context to use for the simulation. + pub sysvar_context: FixtureSysvarContext, + /// The program ID of the program being invoked. + pub program_id: Pubkey, + /// Accounts to pass to the instruction. + pub instruction_accounts: Vec, + /// The instruction data. + pub instruction_data: Vec, + /// Input accounts with state. + pub accounts: Vec<(Pubkey, AccountSharedData)>, +} + +impl TryFrom for FixtureContext { + type Error = FixtureError; + + fn try_from(input: proto::InstrContext) -> Result { + let proto::InstrContext { + compute_budget, + feature_set, + sysvars, + program_id, + instr_accounts, + data: instruction_data, + accounts, + } = input; + + let compute_budget = compute_budget.map(|cb| cb.into()).unwrap_or_default(); + + let feature_set = feature_set.map(|fs| fs.into()).unwrap_or_default(); + + let sysvar_context = sysvars + .map(|sysvars| sysvars.try_into()) + .transpose()? + .unwrap_or_default(); + + let program_id = Pubkey::new_from_array( + program_id + .try_into() + .map_err(|_| FixtureError::InvalidPubkeyBytes)?, + ); + + let accounts = accounts + .into_iter() + .map(|acct_state| acct_state.try_into()) + .collect::, _>>()?; + + let instruction_accounts = instr_accounts + .into_iter() + .map( + |proto::InstrAcct { + index, + is_signer, + is_writable, + }| { + accounts + .get(index as usize) + .ok_or(FixtureError::AccountMissing) + .map(|(pubkey, _)| AccountMeta { + pubkey: *pubkey, + is_signer, + is_writable, + }) + }, + ) + .collect::, _>>()?; + + Ok(Self { + compute_budget, + feature_set, + sysvar_context, + program_id, + instruction_accounts, + instruction_data, + accounts, + }) + } +} diff --git a/fuzz/src/fixture/effects.rs b/fuzz/src/fixture/effects.rs new file mode 100644 index 0000000..94ab35d --- /dev/null +++ b/fuzz/src/fixture/effects.rs @@ -0,0 +1,118 @@ +//! Effects of a single instruction. + +use { + super::{error::FixtureError, proto}, + solana_sdk::{account::AccountSharedData, pubkey::Pubkey}, +}; + +/// Represents the effects of a single instruction. +#[derive(Debug)] +pub struct FixtureEffects { + /// Compute units consumed by the instruction. + pub compute_units_consumed: u64, + /// Execution time for instruction. + pub execution_time: u64, + // Program return code. Zero is success, errors are non-zero. + pub program_result: u32, + /// Resulting accounts with state, to be checked post-simulation. + pub resulting_accounts: Vec<(Pubkey, AccountSharedData)>, +} + +impl TryFrom for FixtureEffects { + type Error = FixtureError; + + fn try_from(input: proto::InstrEffects) -> Result { + let proto::InstrEffects { + compute_units_consumed, + execution_time, + program_result, + resulting_accounts, + } = input; + + let resulting_accounts = resulting_accounts + .into_iter() + .map(|acct_state| acct_state.try_into()) + .collect::, _>>()?; + + Ok(Self { + compute_units_consumed, + execution_time, + program_result, + resulting_accounts, + }) + } +} + +#[cfg(test)] +mod tests { + use {super::*, solana_sdk::account::Account}; + + #[test] + fn test_try_from_proto_instr_effects() { + let address1 = Pubkey::new_unique(); + let owner1 = Pubkey::new_unique(); + let address2 = Pubkey::new_unique(); + let owner2 = Pubkey::new_unique(); + + let compute_units_consumed = 50_000; + let execution_time = 100; + let program_result = 0; + let resulting_accounts = vec![ + proto::AcctState { + address: address1.to_bytes().to_vec(), + owner: owner1.to_bytes().to_vec(), + lamports: 42, + data: vec![1, 2, 3], + executable: false, + rent_epoch: 0, + }, + proto::AcctState { + address: address2.to_bytes().to_vec(), + owner: owner2.to_bytes().to_vec(), + lamports: 42, + data: vec![5, 4, 3], + executable: true, + rent_epoch: 0, + }, + ]; + + let input = proto::InstrEffects { + compute_units_consumed, + execution_time, + program_result, + resulting_accounts, + }; + + let effects = FixtureEffects::try_from(input).unwrap(); + assert_eq!(effects.compute_units_consumed, compute_units_consumed); + assert_eq!(effects.execution_time, execution_time); + assert_eq!(effects.program_result, program_result); + assert_eq!(effects.resulting_accounts.len(), 2); + + let (pubkey, account) = &effects.resulting_accounts[0]; + assert_eq!(*pubkey, address1); + assert_eq!( + *account, + AccountSharedData::from(Account { + lamports: 42, + data: vec![1, 2, 3], + owner: owner1, + executable: false, + rent_epoch: 0, + }) + ); + + let (pubkey, account) = &effects.resulting_accounts[1]; + assert_eq!(*pubkey, address2); + assert_eq!( + *account, + AccountSharedData::from(Account { + lamports: 42, + data: vec![5, 4, 3], + owner: owner2, + executable: true, + rent_epoch: 0, + }) + ); + } +} diff --git a/fuzz/src/fixture/error.rs b/fuzz/src/fixture/error.rs new file mode 100644 index 0000000..cecf011 --- /dev/null +++ b/fuzz/src/fixture/error.rs @@ -0,0 +1,34 @@ +//! Errors possible for parsing fixtures. + +use thiserror::Error; + +/// Errors possible for parsing fixtures. +#[allow(clippy::enum_variant_names)] +#[derive(Debug, Error, PartialEq)] +pub enum FixtureError { + /// Invalid protobuf bytes provided. + #[error("Invalid protobuf")] + InvalidProtobuf(#[from] prost::DecodeError), + /// A provided integer is out of range. + #[error("Integer out of range")] + IntegerOutOfRange, + /// A provided byte array is invalid for a `u128`. + #[error("Invalid u128 bytes")] + InvalidU128Bytes, + /// A provided byte array is invalid for a `Hash`. + #[error("Invalid hash bytes")] + InvalidHashBytes, + /// A provided byte array is invalid for a `Pubkey`. + #[error("Invalid public key bytes")] + InvalidPubkeyBytes, + /// An account index of an instruction account refers to an account that + /// is not present in the input accounts list. + #[error("Account missing")] + AccountMissing, + /// The input fixture is invalid. + #[error("Invalid fixture input")] + InvalidFixtureInput, + /// The output fixture is invalid. + #[error("Invalid fixture output")] + InvalidFixtureOutput, +} diff --git a/fuzz/src/fixture/feature_set.rs b/fuzz/src/fixture/feature_set.rs new file mode 100644 index 0000000..7256e88 --- /dev/null +++ b/fuzz/src/fixture/feature_set.rs @@ -0,0 +1,90 @@ +//! A Solana runtime feature set, as represented in the Solana SDK. + +use {super::proto, solana_sdk::feature_set::*}; + +#[derive(Debug)] +pub struct FixtureFeatureSet { + pub features: Vec, +} + +impl Default for FixtureFeatureSet { + fn default() -> Self { + Self::from(&FeatureSet::all_enabled()) + } +} + +impl From for FixtureFeatureSet { + fn from(input: proto::FeatureSet) -> Self { + Self { + features: input.features, + } + } +} + +impl From for FeatureSet { + fn from(input: FixtureFeatureSet) -> Self { + let mut feature_set = FeatureSet::default(); + for id in std::mem::take(&mut feature_set.inactive).iter() { + let discriminator = u64::from_le_bytes(id.to_bytes()[..8].try_into().unwrap()); + if input.features.contains(&discriminator) { + feature_set.activate(id, 0); + } + } + feature_set + } +} + +impl From<&FeatureSet> for FixtureFeatureSet { + fn from(input: &FeatureSet) -> Self { + let features = input + .active + .keys() + .map(|id| u64::from_le_bytes(id.to_bytes()[..8].try_into().unwrap())) + .collect(); + Self { features } + } +} + +#[cfg(test)] +mod tests { + use {super::*, solana_sdk::pubkey::Pubkey}; + + #[test] + fn test_from_proto_feature_set() { + let try_conversion = |feature_ids: &[Pubkey]| { + let features = feature_ids + .iter() + .map(|id| u64::from_le_bytes(id.to_bytes()[..8].try_into().unwrap())) + .collect::>(); + let proto = proto::FeatureSet { features }; + let fixture = FixtureFeatureSet::from(proto); + FeatureSet::from(fixture) + }; + + // Success + let features = &[ + vote_state_add_vote_latency::id(), + checked_arithmetic_in_fee_validation::id(), + last_restart_slot_sysvar::id(), + reduce_stake_warmup_cooldown::id(), + enable_poseidon_syscall::id(), + require_rent_exempt_split_destination::id(), + better_error_codes_for_tx_lamport_check::id(), + ]; + let feature_set = try_conversion(features); + for feature in features { + assert!(feature_set.is_active(feature)); + } + + // Not valid features (not in the list) + let features = &[ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + let feature_set = try_conversion(features); + for feature in features { + assert!(!feature_set.is_active(feature)); + } + } +} diff --git a/fuzz/src/fixture/mod.rs b/fuzz/src/fixture/mod.rs new file mode 100644 index 0000000..991819e --- /dev/null +++ b/fuzz/src/fixture/mod.rs @@ -0,0 +1,50 @@ +//! A fixture for invoking a single instruction against a simulated Solana +//! program runtime environment, for a given program. + +pub mod account; +pub mod compute_budget; +pub mod context; +pub mod effects; +pub mod error; +pub mod feature_set; +mod proto { + include!(concat!(env!("OUT_DIR"), "/org.mollusk.svm.rs")); +} +pub mod sysvars; + +use {context::FixtureContext, effects::FixtureEffects, error::FixtureError, prost::Message}; + +/// A fixture for invoking a single instruction against a simulated Solana +/// program runtime environment, for a given program. +#[derive(Debug)] +pub struct Fixture { + /// The fixture inputs. + pub input: FixtureContext, + /// The fixture outputs. + pub output: FixtureEffects, +} + +impl TryFrom for Fixture { + type Error = FixtureError; + + fn try_from(fixture: proto::InstrFixture) -> Result { + // All blobs should have an input and output. + let input: FixtureContext = fixture + .input + .ok_or::(FixtureError::InvalidFixtureInput)? + .try_into()?; + let output: FixtureEffects = fixture + .output + .ok_or::(FixtureError::InvalidFixtureOutput)? + .try_into()?; + Ok(Self { input, output }) + } +} + +impl Fixture { + /// Decode a `Protobuf` blob into a `Fixture`. + pub fn decode(blob: &[u8]) -> Result { + let fixture: proto::InstrFixture = proto::InstrFixture::decode(blob)?; + fixture.try_into() + } +} diff --git a/fuzz/src/fixture/sysvars.rs b/fuzz/src/fixture/sysvars.rs new file mode 100644 index 0000000..9a209cc --- /dev/null +++ b/fuzz/src/fixture/sysvars.rs @@ -0,0 +1,378 @@ +//! Solana runtime sysvars, as represented in the Solana SDK. + +use { + super::{error::FixtureError, proto}, + solana_sdk::{ + clock::Clock, + epoch_rewards::EpochRewards, + epoch_schedule::EpochSchedule, + hash::Hash, + rent::Rent, + slot_hashes::{SlotHash, SlotHashes}, + stake_history::{StakeHistory, StakeHistoryEntry}, + }, +}; + +fn try_to_hash(bytes: &[u8]) -> Result { + <[u8; 32]>::try_from(bytes) + .map_err(|_| FixtureError::InvalidHashBytes) + .map(Hash::new_from_array) +} + +fn try_read_u128(bytes: &[u8]) -> Result { + bytes + .try_into() + .map_err(|_| FixtureError::InvalidU128Bytes) + .map(u128::from_le_bytes) +} + +/// A fixture containing the Solana runtime sysvars. +#[derive(Debug, Default)] +pub struct FixtureSysvarContext { + /// `Clock` sysvar. + pub clock: Clock, + /// `EpochRewards` sysvar. + pub epoch_rewards: EpochRewards, + /// `EpochSchedule` sysvar. + pub epoch_schedule: EpochSchedule, + /// `Rent` sysvar. + pub rent: Rent, + /// `SlotHashes` sysvar. + pub slot_hashes: SlotHashes, + /// `StakeHistory` sysvar. + pub stake_history: StakeHistory, +} + +impl From for Clock { + fn from(input: proto::Clock) -> Self { + Self { + slot: input.slot, + epoch_start_timestamp: input.epoch_start_timestamp, + epoch: input.epoch, + leader_schedule_epoch: input.leader_schedule_epoch, + unix_timestamp: input.unix_timestamp, + } + } +} + +impl TryFrom for EpochRewards { + type Error = FixtureError; + + fn try_from(input: proto::EpochRewards) -> Result { + let parent_blockhash = try_to_hash(&input.parent_blockhash)?; + let total_points = try_read_u128(&input.total_points)?; + Ok(Self { + distribution_starting_block_height: input.distribution_starting_block_height, + num_partitions: input.num_partitions, + parent_blockhash, + total_points, + total_rewards: input.total_rewards, + distributed_rewards: input.distributed_rewards, + active: input.active, + }) + } +} + +impl From for EpochSchedule { + fn from(input: proto::EpochSchedule) -> Self { + Self { + slots_per_epoch: input.slots_per_epoch, + leader_schedule_slot_offset: input.leader_schedule_slot_offset, + warmup: input.warmup, + first_normal_epoch: input.first_normal_epoch, + first_normal_slot: input.first_normal_slot, + } + } +} + +impl TryFrom for Rent { + type Error = FixtureError; + + fn try_from(input: proto::Rent) -> Result { + let burn_percent = + u8::try_from(input.burn_percent).map_err(|_| FixtureError::IntegerOutOfRange)?; + Ok(Self { + lamports_per_byte_year: input.lamports_per_byte_year, + exemption_threshold: input.exemption_threshold, + burn_percent, + }) + } +} + +impl TryFrom for SlotHash { + type Error = FixtureError; + + fn try_from(input: proto::SlotHashEntry) -> Result { + let hash = Hash::new_from_array( + input + .hash + .try_into() + .map_err(|_| FixtureError::InvalidHashBytes)?, + ); + Ok((input.slot, hash)) + } +} + +impl TryFrom for SlotHashes { + type Error = FixtureError; + + fn try_from(input: proto::SlotHashes) -> Result { + let slot_hashes: Vec = input + .slot_hashes + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?; + Ok(Self::new(&slot_hashes)) + } +} + +impl From for (u64, StakeHistoryEntry) { + fn from(input: proto::StakeHistoryEntry) -> (u64, StakeHistoryEntry) { + ( + input.epoch, + StakeHistoryEntry { + effective: input.effective, + activating: input.activating, + deactivating: input.deactivating, + }, + ) + } +} + +impl From for StakeHistory { + fn from(input: proto::StakeHistory) -> Self { + let mut stake_history = StakeHistory::default(); + for (epoch, entry) in input.stake_history.into_iter().map(Into::into) { + stake_history.add(epoch, entry); + } + stake_history + } +} + +impl TryFrom for FixtureSysvarContext { + type Error = FixtureError; + + fn try_from(input: proto::SysvarContext) -> Result { + Ok(Self { + clock: input.clock.map(Into::into).unwrap_or_default(), + epoch_rewards: input + .epoch_rewards + .map(TryInto::try_into) + .transpose()? + .unwrap_or_default(), + epoch_schedule: input.epoch_schedule.map(Into::into).unwrap_or_default(), + rent: input + .rent + .map(TryInto::try_into) + .transpose()? + .unwrap_or_default(), + slot_hashes: input + .slot_hashes + .map(TryInto::try_into) + .transpose()? + .unwrap_or_default(), + stake_history: input.stake_history.map(Into::into).unwrap_or_default(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_proto_clock() { + let input = proto::Clock { + slot: 42, + epoch_start_timestamp: 1_000_000, + epoch: 1, + leader_schedule_epoch: 1, + unix_timestamp: 1_000_000, + }; + let clock = Clock::from(input); + assert_eq!(clock.slot, 42); + assert_eq!(clock.epoch_start_timestamp, 1_000_000); + assert_eq!(clock.epoch, 1); + assert_eq!(clock.leader_schedule_epoch, 1); + assert_eq!(clock.unix_timestamp, 1_000_000); + } + + #[test] + fn test_from_proto_epoch_rewards() { + let input = proto::EpochRewards { + distribution_starting_block_height: 42, + num_partitions: 42, + parent_blockhash: vec![0; 32], + total_points: 16u128.to_le_bytes().to_vec(), + total_rewards: 42, + distributed_rewards: 42, + active: true, + }; + let epoch_rewards = EpochRewards::try_from(input).unwrap(); + assert_eq!(epoch_rewards.distribution_starting_block_height, 42); + assert_eq!(epoch_rewards.num_partitions, 42); + assert_eq!(epoch_rewards.parent_blockhash, Hash::default()); + assert_eq!(epoch_rewards.total_points, 16); + assert_eq!(epoch_rewards.total_rewards, 42); + assert_eq!(epoch_rewards.distributed_rewards, 42); + } + + #[test] + fn test_from_proto_epoch_schedule() { + let input = proto::EpochSchedule { + slots_per_epoch: 42, + leader_schedule_slot_offset: 42, + warmup: false, + first_normal_epoch: 42, + first_normal_slot: 42, + }; + let epoch_schedule = EpochSchedule::from(input); + assert_eq!(epoch_schedule.slots_per_epoch, 42); + assert_eq!(epoch_schedule.leader_schedule_slot_offset, 42); + assert!(!epoch_schedule.warmup); + assert_eq!(epoch_schedule.first_normal_epoch, 42); + assert_eq!(epoch_schedule.first_normal_slot, 42); + } + + #[test] + fn test_try_from_proto_rent() { + let input = proto::Rent { + lamports_per_byte_year: 42, + exemption_threshold: 42.0, + burn_percent: 42, + }; + let rent = Rent::try_from(input).unwrap(); + assert_eq!(rent.lamports_per_byte_year, 42); + assert_eq!(rent.exemption_threshold, 42.0); + assert_eq!(rent.burn_percent, 42); + + // Fail integer out of range + let input = proto::Rent { + lamports_per_byte_year: 42, + exemption_threshold: 42.0, + burn_percent: 256, + }; + assert_eq!( + Rent::try_from(input).unwrap_err(), + FixtureError::IntegerOutOfRange + ); + } + + #[test] + fn test_try_from_proto_slot_hash_entry() { + let input = proto::SlotHashEntry { + slot: 42, + hash: vec![0; 32], + }; + let slot_hash = SlotHash::try_from(input).unwrap(); + assert_eq!(slot_hash.0, 42); + assert_eq!(slot_hash.1, Hash::default()); + + // Fail invalid hash bytes + let input = proto::SlotHashEntry { + slot: 42, + hash: vec![0; 31], + }; + assert_eq!( + SlotHash::try_from(input).unwrap_err(), + FixtureError::InvalidHashBytes + ); + } + + #[test] + fn test_try_from_proto_slot_hashes() { + let input = proto::SlotHashes { + slot_hashes: vec![proto::SlotHashEntry { + slot: 42, + hash: vec![0; 32], + }], + }; + let slot_hashes = SlotHashes::try_from(input).unwrap(); + assert_eq!(slot_hashes.len(), 1); + assert_eq!(slot_hashes.get(&42), Some(&Hash::default())); + } + + #[test] + fn test_from_proto_stake_history_entry() { + let input = proto::StakeHistoryEntry { + epoch: 42, + effective: 42, + activating: 42, + deactivating: 42, + }; + let (epoch, entry) = <(u64, StakeHistoryEntry)>::from(input); + assert_eq!(epoch, 42); + assert_eq!(entry.effective, 42); + assert_eq!(entry.activating, 42); + assert_eq!(entry.deactivating, 42); + } + + #[test] + fn test_from_proto_stake_history() { + let input = proto::StakeHistory { + stake_history: vec![proto::StakeHistoryEntry { + epoch: 42, + effective: 42, + activating: 42, + deactivating: 42, + }], + }; + let stake_history = StakeHistory::from(input); + assert_eq!(stake_history.get(42).unwrap().effective, 42); + } + + #[test] + fn test_try_from_proto_sysvar_context() { + let input = proto::SysvarContext { + clock: Some(proto::Clock { + slot: 42, + epoch_start_timestamp: 1_000_000, + epoch: 1, + leader_schedule_epoch: 1, + unix_timestamp: 1_000_000, + }), + epoch_rewards: Some(proto::EpochRewards { + distribution_starting_block_height: 42, + num_partitions: 42, + parent_blockhash: vec![0; 32], + total_points: vec![1; 16], + total_rewards: 42, + distributed_rewards: 42, + active: true, + }), + epoch_schedule: Some(proto::EpochSchedule { + slots_per_epoch: 42, + leader_schedule_slot_offset: 42, + warmup: false, + first_normal_epoch: 42, + first_normal_slot: 42, + }), + rent: Some(proto::Rent { + lamports_per_byte_year: 42, + exemption_threshold: 42.0, + burn_percent: 42, + }), + slot_hashes: Some(proto::SlotHashes { + slot_hashes: vec![proto::SlotHashEntry { + slot: 42, + hash: vec![0; 32], + }], + }), + stake_history: Some(proto::StakeHistory { + stake_history: vec![proto::StakeHistoryEntry { + epoch: 42, + effective: 42, + activating: 42, + deactivating: 42, + }], + }), + }; + let sysvar_context = FixtureSysvarContext::try_from(input).unwrap(); + assert_eq!(sysvar_context.clock.slot, 42); + assert_eq!(sysvar_context.epoch_rewards.total_rewards, 42); + assert_eq!(sysvar_context.epoch_schedule.slots_per_epoch, 42); + assert_eq!(sysvar_context.rent.lamports_per_byte_year, 42); + assert_eq!(sysvar_context.slot_hashes.get(&42), Some(&Hash::default())); + assert_eq!(sysvar_context.stake_history.get(42).unwrap().effective, 42); + } +} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index caae146..f433cb8 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -4,3 +4,5 @@ //! Mollusk, it is not required. Developers can use this fuzz harness on their //! own custom SVM entrypoint. Hence the distinction between fixture types and //! Mollusk types. + +pub mod fixture; From e627df567eed4fba8ffb52a92c99c1bfef3b1f7e Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 16:36:27 +0800 Subject: [PATCH 04/13] support agave feature sets --- fuzz/src/fixture/feature_set.rs | 314 ++++++++++++++++++++++++++++++-- 1 file changed, 299 insertions(+), 15 deletions(-) diff --git a/fuzz/src/fixture/feature_set.rs b/fuzz/src/fixture/feature_set.rs index 7256e88..d2b76f0 100644 --- a/fuzz/src/fixture/feature_set.rs +++ b/fuzz/src/fixture/feature_set.rs @@ -1,6 +1,26 @@ //! A Solana runtime feature set, as represented in the Solana SDK. -use {super::proto, solana_sdk::feature_set::*}; +use { + super::proto, + solana_sdk::{feature_set::*, pubkey::Pubkey}, +}; + +fn discriminator(pubkey: &Pubkey) -> u64 { + u64::from_le_bytes(pubkey.to_bytes()[..8].try_into().unwrap()) +} + +fn create_feature_set<'a>( + declared: impl Iterator, + evaluate: impl Fn(&Pubkey) -> bool, +) -> FeatureSet { + let mut feature_set = FeatureSet::default(); + for id in declared { + if evaluate(id) { + feature_set.activate(id, 0); + } + } + feature_set +} #[derive(Debug)] pub struct FixtureFeatureSet { @@ -23,31 +43,295 @@ impl From for FixtureFeatureSet { impl From for FeatureSet { fn from(input: FixtureFeatureSet) -> Self { - let mut feature_set = FeatureSet::default(); - for id in std::mem::take(&mut feature_set.inactive).iter() { - let discriminator = u64::from_le_bytes(id.to_bytes()[..8].try_into().unwrap()); - if input.features.contains(&discriminator) { - feature_set.activate(id, 0); - } - } - feature_set + create_feature_set(FeatureSet::default().inactive.iter(), |id| { + input.features.contains(&discriminator(id)) + }) } } impl From<&FeatureSet> for FixtureFeatureSet { fn from(input: &FeatureSet) -> Self { - let features = input - .active - .keys() - .map(|id| u64::from_le_bytes(id.to_bytes()[..8].try_into().unwrap())) - .collect(); + let features = input.active.keys().map(discriminator).collect(); Self { features } } } +// Lists of agave supported feature flags, as of `2.0.13`. +// Inactive on all clusters. +static AGAVE_FEATURES_INACTIVE: &[Pubkey] = &[ + full_inflation::devnet_and_testnet::id(), + zk_token_sdk_enabled::id(), + stake_redelegate_instruction::id(), + enable_partitioned_epoch_reward::id(), + partitioned_epoch_rewards_superfeature::id(), + stake_minimum_delegation_for_rewards::id(), + skip_rent_rewrites::id(), + loosen_cpi_size_restriction::id(), + disable_turbine_fanout_experiments::id(), + enable_big_mod_exp_syscall::id(), + apply_cost_tracker_during_replay::id(), + bpf_account_data_direct_mapping::id(), + include_loaded_accounts_data_size_in_fee_calculation::id(), + remaining_compute_units_syscall_enabled::id(), + enable_program_runtime_v2_and_loader_v4::id(), + disable_rent_fees_collection::id(), + enable_zk_transfer_with_fee::id(), + add_new_reserved_account_keys::id(), + enable_zk_proof_from_account::id(), + cost_model_requested_write_lock_cost::id(), + chained_merkle_conflict_duplicate_proofs::id(), + remove_rounding_in_fee_calculation::id(), + enable_tower_sync_ix::id(), + reward_full_priority_fee::id(), + get_sysvar_syscall_enabled::id(), + abort_on_invalid_curve::id(), + migrate_feature_gate_program_to_core_bpf::id(), + vote_only_full_fec_sets::id(), + migrate_config_program_to_core_bpf::id(), + enable_get_epoch_stake_syscall::id(), + migrate_address_lookup_table_program_to_core_bpf::id(), + zk_elgamal_proof_program_enabled::id(), + move_stake_and_move_lamports_ixs::id(), + ed25519_precompile_verify_strict::id(), + verify_retransmitter_signature::id(), + vote_only_retransmitter_signed_fec_sets::id(), +]; +// Active on testnet. +static AGAVE_FEATURES_TESTNET: &[Pubkey] = &[]; +// Active on devnet. +static AGAVE_FEATURES_DEVNET: &[Pubkey] = &[ + blake3_syscall_enabled::id(), + libsecp256k1_fail_on_bad_count::id(), + increase_tx_account_lock_limit::id(), + timely_vote_credits::id(), + allow_commission_decrease_at_any_time::id(), + enable_chained_merkle_shreds::id(), +]; +// Active on mainnet-beta. +static AGAVE_FEATURES_MAINNET_BETA: &[Pubkey] = &[ + deprecate_rewards_sysvar::id(), + pico_inflation::id(), + full_inflation::mainnet::certusone::vote::id(), + full_inflation::mainnet::certusone::enable::id(), + secp256k1_program_enabled::id(), + spl_token_v2_multisig_fix::id(), + no_overflow_rent_distribution::id(), + filter_stake_delegation_accounts::id(), + require_custodian_for_locked_stake_authorize::id(), + spl_token_v2_self_transfer_fix::id(), + warp_timestamp_again::id(), + check_init_vote_data::id(), + secp256k1_recover_syscall_enabled::id(), + system_transfer_zero_check::id(), + dedupe_config_program_signers::id(), + verify_tx_signatures_len::id(), + vote_stake_checked_instructions::id(), + rent_for_sysvars::id(), + libsecp256k1_0_5_upgrade_enabled::id(), + tx_wide_compute_cap::id(), + spl_token_v2_set_authority_fix::id(), + merge_nonce_error_into_system_error::id(), + disable_fees_sysvar::id(), + stake_merge_with_unmatched_credits_observed::id(), + curve25519_syscall_enabled::id(), + curve25519_restrict_msm_length::id(), + versioned_tx_message_enabled::id(), + libsecp256k1_fail_on_bad_count2::id(), + instructions_sysvar_owned_by_sysvar::id(), + stake_program_advance_activating_credits_observed::id(), + credits_auto_rewind::id(), + demote_program_write_locks::id(), + ed25519_program_enabled::id(), + return_data_syscall_enabled::id(), + reduce_required_deploy_balance::id(), + sol_log_data_syscall_enabled::id(), + stakes_remove_delegation_if_inactive::id(), + do_support_realloc::id(), + prevent_calling_precompiles_as_programs::id(), + optimize_epoch_boundary_updates::id(), + remove_native_loader::id(), + send_to_tpu_vote_port::id(), + requestable_heap_size::id(), + disable_fee_calculator::id(), + add_compute_budget_program::id(), + nonce_must_be_writable::id(), + spl_token_v3_3_0_release::id(), + leave_nonce_on_success::id(), + reject_empty_instruction_without_program::id(), + fixed_memcpy_nonoverlapping_check::id(), + reject_non_rent_exempt_vote_withdraws::id(), + evict_invalid_stakes_cache_entries::id(), + allow_votes_to_directly_update_vote_state::id(), + max_tx_account_locks::id(), + require_rent_exempt_accounts::id(), + filter_votes_outside_slot_hashes::id(), + update_syscall_base_costs::id(), + stake_deactivate_delinquent_instruction::id(), + vote_withdraw_authority_may_change_authorized_voter::id(), + spl_associated_token_account_v1_0_4::id(), + reject_vote_account_close_unless_zero_credit_epoch::id(), + add_get_processed_sibling_instruction_syscall::id(), + bank_transaction_count_fix::id(), + disable_bpf_deprecated_load_instructions::id(), + disable_bpf_unresolved_symbols_at_runtime::id(), + record_instruction_in_transaction_context_push::id(), + syscall_saturated_math::id(), + check_physical_overlapping::id(), + limit_secp256k1_recovery_id::id(), + disable_deprecated_loader::id(), + check_slice_translation_size::id(), + stake_split_uses_rent_sysvar::id(), + add_get_minimum_delegation_instruction_to_stake_program::id(), + error_on_syscall_bpf_function_hash_collisions::id(), + reject_callx_r10::id(), + drop_redundant_turbine_path::id(), + executables_incur_cpi_data_cost::id(), + fix_recent_blockhashes::id(), + update_rewards_from_cached_accounts::id(), + spl_token_v3_4_0::id(), + spl_associated_token_account_v1_1_0::id(), + default_units_per_instruction::id(), + stake_allow_zero_undelegated_amount::id(), + require_static_program_ids_in_transaction::id(), + add_set_compute_unit_price_ix::id(), + disable_deploy_of_alloc_free_syscall::id(), + include_account_index_in_rent_error::id(), + add_shred_type_to_shred_seed::id(), + warp_timestamp_with_a_vengeance::id(), + separate_nonce_from_blockhash::id(), + enable_durable_nonce::id(), + vote_state_update_credit_per_dequeue::id(), + quick_bail_on_panic::id(), + nonce_must_be_authorized::id(), + nonce_must_be_advanceable::id(), + vote_authorize_with_seed::id(), + preserve_rent_epoch_for_rent_exempt_accounts::id(), + enable_bpf_loader_extend_program_ix::id(), + enable_early_verification_of_account_modifications::id(), + prevent_crediting_accounts_that_end_rent_paying::id(), + cap_bpf_program_instruction_accounts::id(), + use_default_units_in_fee_calculation::id(), + compact_vote_state_updates::id(), + incremental_snapshot_only_incremental_hash_calculation::id(), + disable_cpi_setting_executable_and_rent_epoch::id(), + on_load_preserve_rent_epoch_for_rent_exempt_accounts::id(), + account_hash_ignore_slot::id(), + set_exempt_rent_epoch_max::id(), + relax_authority_signer_check_for_lookup_table_creation::id(), + stop_sibling_instruction_search_at_parent::id(), + vote_state_update_root_fix::id(), + cap_accounts_data_allocations_per_transaction::id(), + epoch_accounts_hash::id(), + remove_deprecated_request_unit_ix::id(), + disable_rehash_for_rent_epoch::id(), + limit_max_instruction_trace_length::id(), + check_syscall_outputs_do_not_overlap::id(), + enable_bpf_loader_set_authority_checked_ix::id(), + enable_alt_bn128_syscall::id(), + simplify_alt_bn128_syscall_error_codes::id(), + enable_alt_bn128_compression_syscall::id(), + enable_program_redeployment_cooldown::id(), + commission_updates_only_allowed_in_first_half_of_epoch::id(), + enable_turbine_fanout_experiments::id(), + move_serialized_len_ptr_in_cpi::id(), + update_hashes_per_tick::id(), + disable_builtin_loader_ownership_chains::id(), + cap_transaction_accounts_data_size::id(), + remove_congestion_multiplier_from_fee_calculation::id(), + enable_request_heap_frame_ix::id(), + prevent_rent_paying_rent_recipients::id(), + delay_visibility_of_program_deployment::id(), + add_set_tx_loaded_accounts_data_size_instruction::id(), + switch_to_new_elf_parser::id(), + round_up_heap_size::id(), + remove_bpf_loader_incorrect_program_id::id(), + native_programs_consume_cu::id(), + simplify_writable_program_account_check::id(), + stop_truncating_strings_in_syscalls::id(), + clean_up_delegation_errors::id(), + vote_state_add_vote_latency::id(), + checked_arithmetic_in_fee_validation::id(), + last_restart_slot_sysvar::id(), + reduce_stake_warmup_cooldown::id(), + enable_poseidon_syscall::id(), + require_rent_exempt_split_destination::id(), + better_error_codes_for_tx_lamport_check::id(), + update_hashes_per_tick2::id(), + update_hashes_per_tick3::id(), + update_hashes_per_tick4::id(), + update_hashes_per_tick5::id(), + update_hashes_per_tick6::id(), + validate_fee_collector_account::id(), + drop_legacy_shreds::id(), + consume_blockstore_duplicate_proofs::id(), + index_erasure_conflict_duplicate_proofs::id(), + merkle_conflict_duplicate_proofs::id(), + disable_bpf_loader_instructions::id(), + enable_gossip_duplicate_proof_ingestion::id(), + deprecate_unused_legacy_vote_plumbing::id(), +]; + +/// Agave's currently active feature sets. +pub trait AgaveFeatures { + fn mainnet_beta() -> Self; + fn devnet() -> Self; + fn testnet() -> Self; + fn inactive() -> Self; + fn all() -> Self; +} + +impl AgaveFeatures for FeatureSet { + fn mainnet_beta() -> Self { + create_feature_set(AGAVE_FEATURES_MAINNET_BETA.iter(), |_| true) + } + + fn devnet() -> Self { + create_feature_set(AGAVE_FEATURES_DEVNET.iter(), |_| true) + } + + fn testnet() -> Self { + create_feature_set(AGAVE_FEATURES_TESTNET.iter(), |_| true) + } + + fn inactive() -> Self { + create_feature_set(AGAVE_FEATURES_INACTIVE.iter(), |_| true) + } + + fn all() -> Self { + let features = AGAVE_FEATURES_MAINNET_BETA + .iter() + .chain(AGAVE_FEATURES_DEVNET.iter()) + .chain(AGAVE_FEATURES_TESTNET.iter()) + .chain(AGAVE_FEATURES_INACTIVE.iter()); + create_feature_set(features, |_| true) + } +} + +impl AgaveFeatures for FixtureFeatureSet { + fn mainnet_beta() -> Self { + (&::mainnet_beta()).into() + } + + fn devnet() -> Self { + (&::devnet()).into() + } + + fn testnet() -> Self { + (&::testnet()).into() + } + + fn inactive() -> Self { + (&::inactive()).into() + } + + fn all() -> Self { + (&::all()).into() + } +} + #[cfg(test)] mod tests { - use {super::*, solana_sdk::pubkey::Pubkey}; + use super::*; #[test] fn test_from_proto_feature_set() { From 7a390ca55ed571a6b44fdc299fed6fdcd299805c Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 15:22:10 +0800 Subject: [PATCH 05/13] fixture converters that go in reverse --- fuzz/src/fixture/account.rs | 16 ++++- fuzz/src/fixture/compute_budget.rs | 97 +++++++++++++++++++++++++++++ fuzz/src/fixture/context.rs | 55 ++++++++++++++++- fuzz/src/fixture/effects.rs | 21 +++++++ fuzz/src/fixture/feature_set.rs | 8 ++- fuzz/src/fixture/mod.rs | 10 +++ fuzz/src/fixture/sysvars.rs | 98 ++++++++++++++++++++++++++++++ 7 files changed, 302 insertions(+), 3 deletions(-) diff --git a/fuzz/src/fixture/account.rs b/fuzz/src/fixture/account.rs index df0555e..f6e9f6d 100644 --- a/fuzz/src/fixture/account.rs +++ b/fuzz/src/fixture/account.rs @@ -4,7 +4,7 @@ use { super::{error::FixtureError, proto}, solana_sdk::{ - account::{Account, AccountSharedData}, + account::{Account, AccountSharedData, ReadableAccount}, pubkey::Pubkey, }, }; @@ -46,6 +46,20 @@ impl TryFrom for (Pubkey, AccountSharedData) { } } +impl From<&(Pubkey, AccountSharedData)> for proto::AcctState { + fn from(input: &(Pubkey, AccountSharedData)) -> Self { + let (pubkey, account) = input; + Self { + address: pubkey.to_bytes().to_vec(), + owner: account.owner().to_bytes().to_vec(), + lamports: account.lamports(), + data: account.data().to_vec(), + executable: account.executable(), + rent_epoch: account.rent_epoch(), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/fuzz/src/fixture/compute_budget.rs b/fuzz/src/fixture/compute_budget.rs index 346fe4e..e994def 100644 --- a/fuzz/src/fixture/compute_budget.rs +++ b/fuzz/src/fixture/compute_budget.rs @@ -105,3 +105,100 @@ impl From for ComputeBudget { } } } + +impl From<&ComputeBudget> for proto::ComputeBudget { + fn from(input: &ComputeBudget) -> Self { + let ComputeBudget { + compute_unit_limit, + log_64_units, + create_program_address_units, + invoke_units, + max_instruction_stack_depth, + max_instruction_trace_length, + sha256_base_cost, + sha256_byte_cost, + sha256_max_slices, + max_call_depth, + stack_frame_size, + log_pubkey_units, + max_cpi_instruction_size, + cpi_bytes_per_unit, + sysvar_base_cost, + secp256k1_recover_cost, + syscall_base_cost, + curve25519_edwards_validate_point_cost, + curve25519_edwards_add_cost, + curve25519_edwards_subtract_cost, + curve25519_edwards_multiply_cost, + curve25519_edwards_msm_base_cost, + curve25519_edwards_msm_incremental_cost, + curve25519_ristretto_validate_point_cost, + curve25519_ristretto_add_cost, + curve25519_ristretto_subtract_cost, + curve25519_ristretto_multiply_cost, + curve25519_ristretto_msm_base_cost, + curve25519_ristretto_msm_incremental_cost, + heap_size, + heap_cost, + mem_op_base_cost, + alt_bn128_addition_cost, + alt_bn128_multiplication_cost, + alt_bn128_pairing_one_pair_cost_first, + alt_bn128_pairing_one_pair_cost_other, + big_modular_exponentiation_cost, + poseidon_cost_coefficient_a, + poseidon_cost_coefficient_c, + get_remaining_compute_units_cost, + alt_bn128_g1_compress, + alt_bn128_g1_decompress, + alt_bn128_g2_compress, + alt_bn128_g2_decompress, + } = input; + Self { + compute_unit_limit: *compute_unit_limit, + log_64_units: *log_64_units, + create_program_address_units: *create_program_address_units, + invoke_units: *invoke_units, + max_instruction_stack_depth: *max_instruction_stack_depth as u64, + max_instruction_trace_length: *max_instruction_trace_length as u64, + sha256_base_cost: *sha256_base_cost, + sha256_byte_cost: *sha256_byte_cost, + sha256_max_slices: *sha256_max_slices, + max_call_depth: *max_call_depth as u64, + stack_frame_size: *stack_frame_size as u64, + log_pubkey_units: *log_pubkey_units, + max_cpi_instruction_size: *max_cpi_instruction_size as u64, + cpi_bytes_per_unit: *cpi_bytes_per_unit, + sysvar_base_cost: *sysvar_base_cost, + secp256k1_recover_cost: *secp256k1_recover_cost, + syscall_base_cost: *syscall_base_cost, + curve25519_edwards_validate_point_cost: *curve25519_edwards_validate_point_cost, + curve25519_edwards_add_cost: *curve25519_edwards_add_cost, + curve25519_edwards_subtract_cost: *curve25519_edwards_subtract_cost, + curve25519_edwards_multiply_cost: *curve25519_edwards_multiply_cost, + curve25519_edwards_msm_base_cost: *curve25519_edwards_msm_base_cost, + curve25519_edwards_msm_incremental_cost: *curve25519_edwards_msm_incremental_cost, + curve25519_ristretto_validate_point_cost: *curve25519_ristretto_validate_point_cost, + curve25519_ristretto_add_cost: *curve25519_ristretto_add_cost, + curve25519_ristretto_subtract_cost: *curve25519_ristretto_subtract_cost, + curve25519_ristretto_multiply_cost: *curve25519_ristretto_multiply_cost, + curve25519_ristretto_msm_base_cost: *curve25519_ristretto_msm_base_cost, + curve25519_ristretto_msm_incremental_cost: *curve25519_ristretto_msm_incremental_cost, + heap_size: *heap_size, + heap_cost: *heap_cost, + mem_op_base_cost: *mem_op_base_cost, + alt_bn128_addition_cost: *alt_bn128_addition_cost, + alt_bn128_multiplication_cost: *alt_bn128_multiplication_cost, + alt_bn128_pairing_one_pair_cost_first: *alt_bn128_pairing_one_pair_cost_first, + alt_bn128_pairing_one_pair_cost_other: *alt_bn128_pairing_one_pair_cost_other, + big_modular_exponentiation_cost: *big_modular_exponentiation_cost, + poseidon_cost_coefficient_a: *poseidon_cost_coefficient_a, + poseidon_cost_coefficient_c: *poseidon_cost_coefficient_c, + get_remaining_compute_units_cost: *get_remaining_compute_units_cost, + alt_bn128_g1_compress: *alt_bn128_g1_compress, + alt_bn128_g1_decompress: *alt_bn128_g1_decompress, + alt_bn128_g2_compress: *alt_bn128_g2_compress, + alt_bn128_g2_decompress: *alt_bn128_g2_decompress, + } + } +} diff --git a/fuzz/src/fixture/context.rs b/fuzz/src/fixture/context.rs index c50925f..679eb03 100644 --- a/fuzz/src/fixture/context.rs +++ b/fuzz/src/fixture/context.rs @@ -6,7 +6,11 @@ use { error::FixtureError, feature_set::FixtureFeatureSet, proto, sysvars::FixtureSysvarContext, }, solana_compute_budget::compute_budget::ComputeBudget, - solana_sdk::{account::AccountSharedData, instruction::AccountMeta, pubkey::Pubkey}, + solana_sdk::{ + account::{AccountSharedData, ReadableAccount}, + instruction::AccountMeta, + pubkey::Pubkey, + }, }; /// Instruction context fixture. @@ -93,3 +97,52 @@ impl TryFrom for FixtureContext { }) } } + +impl From<&FixtureContext> for proto::InstrContext { + fn from(input: &FixtureContext) -> Self { + let FixtureContext { + compute_budget, + feature_set, + sysvar_context, + program_id, + instruction_accounts, + instruction_data, + accounts, + } = input; + let compute_budget = Some(compute_budget.into()); + let feature_set = Some(feature_set.into()); + let sysvars = Some(sysvar_context.into()); + let program_id = program_id.to_bytes().to_vec(); + let instr_accounts = instruction_accounts + .iter() + .map(|acct| proto::InstrAcct { + index: accounts + .iter() + .position(|(pubkey, _)| *pubkey == acct.pubkey) + .unwrap() as u32, + is_signer: acct.is_signer, + is_writable: acct.is_writable, + }) + .collect::>(); + let accounts = accounts + .iter() + .map(|(pubkey, account)| proto::AcctState { + address: pubkey.to_bytes().to_vec(), + owner: account.owner().to_bytes().to_vec(), + lamports: account.lamports(), + data: account.data().to_vec(), + executable: account.executable(), + rent_epoch: account.rent_epoch(), + }) + .collect::>(); + Self { + compute_budget, + feature_set, + sysvars, + program_id, + instr_accounts, + data: instruction_data.clone(), + accounts, + } + } +} diff --git a/fuzz/src/fixture/effects.rs b/fuzz/src/fixture/effects.rs index 94ab35d..d1a6876 100644 --- a/fuzz/src/fixture/effects.rs +++ b/fuzz/src/fixture/effects.rs @@ -43,6 +43,27 @@ impl TryFrom for FixtureEffects { } } +impl From<&FixtureEffects> for proto::InstrEffects { + fn from(input: &FixtureEffects) -> Self { + let FixtureEffects { + compute_units_consumed, + execution_time, + program_result, + resulting_accounts, + } = input; + let resulting_accounts = resulting_accounts + .iter() + .map(|a| a.into()) + .collect::>(); + Self { + compute_units_consumed: *compute_units_consumed, + execution_time: *execution_time, + program_result: *program_result, + resulting_accounts, + } + } +} + #[cfg(test)] mod tests { use {super::*, solana_sdk::account::Account}; diff --git a/fuzz/src/fixture/feature_set.rs b/fuzz/src/fixture/feature_set.rs index d2b76f0..6c25170 100644 --- a/fuzz/src/fixture/feature_set.rs +++ b/fuzz/src/fixture/feature_set.rs @@ -40,6 +40,13 @@ impl From for FixtureFeatureSet { } } } +impl From<&FixtureFeatureSet> for proto::FeatureSet { + fn from(input: &FixtureFeatureSet) -> Self { + proto::FeatureSet { + features: input.features.clone(), + } + } +} impl From for FeatureSet { fn from(input: FixtureFeatureSet) -> Self { @@ -48,7 +55,6 @@ impl From for FeatureSet { }) } } - impl From<&FeatureSet> for FixtureFeatureSet { fn from(input: &FeatureSet) -> Self { let features = input.active.keys().map(discriminator).collect(); diff --git a/fuzz/src/fixture/mod.rs b/fuzz/src/fixture/mod.rs index 991819e..02eb4ca 100644 --- a/fuzz/src/fixture/mod.rs +++ b/fuzz/src/fixture/mod.rs @@ -41,6 +41,16 @@ impl TryFrom for Fixture { } } +impl From<&Fixture> for proto::InstrFixture { + fn from(fixture: &Fixture) -> Self { + let Fixture { input, output } = fixture; + proto::InstrFixture { + input: Some(input.into()), + output: Some(output.into()), + } + } +} + impl Fixture { /// Decode a `Protobuf` blob into a `Fixture`. pub fn decode(blob: &[u8]) -> Result { diff --git a/fuzz/src/fixture/sysvars.rs b/fuzz/src/fixture/sysvars.rs index 9a209cc..efe1db9 100644 --- a/fuzz/src/fixture/sysvars.rs +++ b/fuzz/src/fixture/sysvars.rs @@ -54,6 +54,17 @@ impl From for Clock { } } } +impl From<&Clock> for proto::Clock { + fn from(input: &Clock) -> Self { + Self { + slot: input.slot, + epoch_start_timestamp: input.epoch_start_timestamp, + epoch: input.epoch, + leader_schedule_epoch: input.leader_schedule_epoch, + unix_timestamp: input.unix_timestamp, + } + } +} impl TryFrom for EpochRewards { type Error = FixtureError; @@ -72,6 +83,19 @@ impl TryFrom for EpochRewards { }) } } +impl From<&EpochRewards> for proto::EpochRewards { + fn from(input: &EpochRewards) -> Self { + Self { + distribution_starting_block_height: input.distribution_starting_block_height, + num_partitions: input.num_partitions, + parent_blockhash: input.parent_blockhash.to_bytes().to_vec(), + total_points: input.total_points.to_le_bytes().to_vec(), + total_rewards: input.total_rewards, + distributed_rewards: input.distributed_rewards, + active: input.active, + } + } +} impl From for EpochSchedule { fn from(input: proto::EpochSchedule) -> Self { @@ -84,6 +108,17 @@ impl From for EpochSchedule { } } } +impl From<&EpochSchedule> for proto::EpochSchedule { + fn from(input: &EpochSchedule) -> Self { + Self { + slots_per_epoch: input.slots_per_epoch, + leader_schedule_slot_offset: input.leader_schedule_slot_offset, + warmup: input.warmup, + first_normal_epoch: input.first_normal_epoch, + first_normal_slot: input.first_normal_slot, + } + } +} impl TryFrom for Rent { type Error = FixtureError; @@ -98,6 +133,15 @@ impl TryFrom for Rent { }) } } +impl From<&Rent> for proto::Rent { + fn from(input: &Rent) -> Self { + Self { + lamports_per_byte_year: input.lamports_per_byte_year, + exemption_threshold: input.exemption_threshold, + burn_percent: input.burn_percent as u32, + } + } +} impl TryFrom for SlotHash { type Error = FixtureError; @@ -112,6 +156,15 @@ impl TryFrom for SlotHash { Ok((input.slot, hash)) } } +impl From<&SlotHash> for proto::SlotHashEntry { + fn from(input: &SlotHash) -> Self { + let (slot, hash) = input; + Self { + slot: *slot, + hash: hash.to_bytes().to_vec(), + } + } +} impl TryFrom for SlotHashes { type Error = FixtureError; @@ -125,6 +178,13 @@ impl TryFrom for SlotHashes { Ok(Self::new(&slot_hashes)) } } +impl From<&SlotHashes> for proto::SlotHashes { + fn from(input: &SlotHashes) -> Self { + Self { + slot_hashes: input.slot_hashes().iter().map(Into::into).collect(), + } + } +} impl From for (u64, StakeHistoryEntry) { fn from(input: proto::StakeHistoryEntry) -> (u64, StakeHistoryEntry) { @@ -138,6 +198,16 @@ impl From for (u64, StakeHistoryEntry) { ) } } +impl From<(u64, StakeHistoryEntry)> for proto::StakeHistoryEntry { + fn from(input: (u64, StakeHistoryEntry)) -> Self { + Self { + epoch: input.0, + effective: input.1.effective, + activating: input.1.activating, + deactivating: input.1.deactivating, + } + } +} impl From for StakeHistory { fn from(input: proto::StakeHistory) -> Self { @@ -148,6 +218,13 @@ impl From for StakeHistory { stake_history } } +impl From<&StakeHistory> for proto::StakeHistory { + fn from(input: &StakeHistory) -> Self { + Self { + stake_history: input.iter().cloned().map(Into::into).collect(), + } + } +} impl TryFrom for FixtureSysvarContext { type Error = FixtureError; @@ -176,6 +253,27 @@ impl TryFrom for FixtureSysvarContext { } } +impl From<&FixtureSysvarContext> for proto::SysvarContext { + fn from(input: &FixtureSysvarContext) -> Self { + let FixtureSysvarContext { + clock, + epoch_rewards, + epoch_schedule, + rent, + slot_hashes, + stake_history, + } = input; + Self { + clock: Some(clock.into()), + epoch_rewards: Some(epoch_rewards.into()), + epoch_schedule: Some(epoch_schedule.into()), + rent: Some(rent.into()), + slot_hashes: Some(slot_hashes.into()), + stake_history: Some(stake_history.into()), + } + } +} + #[cfg(test)] mod tests { use super::*; From 981817160f42ede825e3372a61a7a7b119d1597c Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 15:15:30 +0800 Subject: [PATCH 06/13] helper to load fixtures from a blob file --- fuzz/src/fixture/mod.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/fuzz/src/fixture/mod.rs b/fuzz/src/fixture/mod.rs index 02eb4ca..88ccb8f 100644 --- a/fuzz/src/fixture/mod.rs +++ b/fuzz/src/fixture/mod.rs @@ -12,7 +12,13 @@ mod proto { } pub mod sysvars; -use {context::FixtureContext, effects::FixtureEffects, error::FixtureError, prost::Message}; +use { + context::FixtureContext, + effects::FixtureEffects, + error::FixtureError, + prost::Message, + std::{fs::File, io::Read}, +}; /// A fixture for invoking a single instruction against a simulated Solana /// program runtime environment, for a given program. @@ -57,4 +63,16 @@ impl Fixture { let fixture: proto::InstrFixture = proto::InstrFixture::decode(blob)?; fixture.try_into() } + + /// Loads a `Fixture` from a protobuf binary blob file. + pub fn load_from_blob_file(file_path: &str) -> Result { + if !file_path.ends_with(".fix") { + panic!("Invalid fixture file extension: {}", file_path); + } + let mut file = File::open(file_path).expect("Failed to open fixture file"); + let mut buf = Vec::new(); + file.read_to_end(&mut buf) + .expect("Failed to read fixture file"); + Self::decode(&buf) + } } From 99a60a459dd2cd1b81e332881fd741f1e98ae1be Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 15:24:33 +0800 Subject: [PATCH 07/13] helper to dump fixtures into a blob file --- Cargo.lock | 1 + Cargo.toml | 1 + fuzz/Cargo.toml | 1 + fuzz/src/fixture/mod.rs | 23 ++++++++++++++++++++++- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4dea436..db5f246 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1583,6 +1583,7 @@ dependencies = [ name = "mollusk-svm-fuzz" version = "0.0.4" dependencies = [ + "bs58", "prost", "prost-build", "prost-types", diff --git a/Cargo.toml b/Cargo.toml index bc1ec4c..cabc3bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ version = "0.0.4" [workspace.dependencies] bincode = "1.3.3" +bs58 = "0.5.1" mollusk-svm = { path = "harness", version = "0.0.4" } mollusk-svm-bencher = { path = "bencher", version = "0.0.4" } mollusk-svm-fuzz = { path = "fuzz", version = "0.0.4" } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 011a0c5..79f06b5 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,6 +9,7 @@ edition = { workspace = true } version = { workspace = true } [dependencies] +bs58 = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } solana-compute-budget = { workspace = true } diff --git a/fuzz/src/fixture/mod.rs b/fuzz/src/fixture/mod.rs index 88ccb8f..4c0c27b 100644 --- a/fuzz/src/fixture/mod.rs +++ b/fuzz/src/fixture/mod.rs @@ -17,7 +17,11 @@ use { effects::FixtureEffects, error::FixtureError, prost::Message, - std::{fs::File, io::Read}, + std::{ + fs::{self, File}, + io::{Read, Write}, + path::Path, + }, }; /// A fixture for invoking a single instruction against a simulated Solana @@ -64,6 +68,23 @@ impl Fixture { fixture.try_into() } + /// Dumps the `Fixture` to a protobuf binary blob file. + /// The file name is a hash of the fixture with the `.fix` extension. + pub fn dump_to_blob_file(&self, dir_path: &str) { + let proto_fixture: proto::InstrFixture = self.into(); + let mut buf = Vec::new(); + proto_fixture + .encode(&mut buf) + .expect("Failed to encode fixture"); + let hash = solana_sdk::hash::hash(&buf); + let file_name = format!("instr-{}.fix", bs58::encode(hash).into_string()); + fs::create_dir_all(dir_path).expect("Failed to create directory"); + let file_path = Path::new(dir_path).join(file_name); + let mut file = File::create(file_path).unwrap(); + file.write_all(&buf) + .expect("Failed to write fixture to file"); + } + /// Loads a `Fixture` from a protobuf binary blob file. pub fn load_from_blob_file(file_path: &str) -> Result { if !file_path.ends_with(".fix") { From 814877234a2987012029f591460bd3abedc77abd Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 15:33:24 +0800 Subject: [PATCH 08/13] harness: introduce `fuzz` feature --- Cargo.lock | 1 + harness/Cargo.toml | 4 ++ harness/src/fuzz.rs | 101 +++++++++++++++++++++++++++++++ harness/src/lib.rs | 19 ++++++ harness/tests/dump_fixture.rs | 108 ++++++++++++++++++++++++++++++++++ 5 files changed, 233 insertions(+) create mode 100644 harness/src/fuzz.rs create mode 100644 harness/tests/dump_fixture.rs diff --git a/Cargo.lock b/Cargo.lock index db5f246..5a034b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1558,6 +1558,7 @@ version = "0.0.4" dependencies = [ "bincode", "criterion", + "mollusk-svm-fuzz", "solana-bpf-loader-program", "solana-compute-budget", "solana-logger", diff --git a/harness/Cargo.toml b/harness/Cargo.toml index 0a0af35..b77982c 100644 --- a/harness/Cargo.toml +++ b/harness/Cargo.toml @@ -9,8 +9,12 @@ license = { workspace = true } edition = { workspace = true } version = { workspace = true } +[features] +fuzz = ["dep:mollusk-svm-fuzz"] + [dependencies] bincode = { workspace = true } +mollusk-svm-fuzz = { workspace = true, optional = true } solana-bpf-loader-program = { workspace = true } solana-compute-budget = { workspace = true } solana-program-runtime = { workspace = true } diff --git a/harness/src/fuzz.rs b/harness/src/fuzz.rs new file mode 100644 index 0000000..9178985 --- /dev/null +++ b/harness/src/fuzz.rs @@ -0,0 +1,101 @@ +//! Module for converting from Mollusk SVM Fuzz fixtures to Mollusk types. +//! +//! These conversions allow Mollusk to eject fuzzing fixtures from tests. +//! +//! Only available when the `fuzz` feature is enabled. + +pub use mollusk_svm_fuzz::*; +use { + crate::{ + result::{Check, InstructionResult, ProgramResult}, + sysvar::Sysvars, + Mollusk, + }, + fixture::{ + context::FixtureContext, effects::FixtureEffects, sysvars::FixtureSysvarContext, Fixture, + }, + solana_sdk::{ + account::AccountSharedData, instruction::Instruction, pubkey::Pubkey, + slot_hashes::SlotHashes, + }, +}; + +impl From<&Sysvars> for FixtureSysvarContext { + fn from(input: &Sysvars) -> Self { + let slot_hashes = SlotHashes::new(&input.slot_hashes); + Self { + clock: input.clock.clone(), + epoch_rewards: input.epoch_rewards.clone(), + epoch_schedule: input.epoch_schedule.clone(), + rent: input.rent.clone(), + slot_hashes, + stake_history: input.stake_history.clone(), + } + } +} + +impl From<&InstructionResult> for FixtureEffects { + fn from(input: &InstructionResult) -> Self { + let compute_units_consumed = input.compute_units_consumed; + let execution_time = input.execution_time; + let program_result = match &input.program_result { + ProgramResult::Success => 0, + ProgramResult::Failure(e) => e.clone().into(), + ProgramResult::UnknownError(_) => u64::MAX, //TODO + } as u32; // Also TODO. + + let resulting_accounts = input + .resulting_accounts + .iter() + .map(|(pubkey, account)| (*pubkey, account.clone())) + .collect(); + + Self { + compute_units_consumed, + execution_time, + program_result, + resulting_accounts, + } + } +} + +fn build_fixture_context( + mollusk: &Mollusk, + instruction: &Instruction, + accounts: &[(Pubkey, AccountSharedData)], +) -> FixtureContext { + let Mollusk { + compute_budget, + feature_set, + sysvars, + .. + } = mollusk; + + let instruction_accounts = instruction.accounts.clone(); + let instruction_data = instruction.data.clone(); + let accounts = accounts.to_vec(); + + FixtureContext { + compute_budget: *compute_budget, + feature_set: feature_set.into(), + sysvar_context: sysvars.into(), + program_id: instruction.program_id, + instruction_accounts, + instruction_data, + accounts, + } +} + +pub fn build_fixture_from_mollusk_test( + mollusk: &Mollusk, + instruction: &Instruction, + accounts: &[(Pubkey, AccountSharedData)], + result: &InstructionResult, + _checks: &[Check], +) -> Fixture { + let input = build_fixture_context(mollusk, instruction, accounts); + // This should probably be built from the checks, but there's currently no + // mechanism to enforce full check coverage on a result. + let output = FixtureEffects::from(result); + Fixture { input, output } +} diff --git a/harness/src/lib.rs b/harness/src/lib.rs index 466b226..80480f6 100644 --- a/harness/src/lib.rs +++ b/harness/src/lib.rs @@ -29,6 +29,8 @@ mod error; pub mod file; +#[cfg(feature = "fuzz")] +pub mod fuzz; mod keys; pub mod program; pub mod result; @@ -216,6 +218,16 @@ impl Mollusk { /// Process an instruction using the minified Solana Virtual Machine (SVM) /// environment, then perform checks on the result. Panics if any checks /// fail. + /// + /// For `fuzz` feature only: + /// + /// If the `EJECT_FUZZ_FIXTURES` environment variable is set, this function + /// will convert the provided test to a fuzz fixture and write it to the + /// provided directory. + /// + /// ```ignore + /// EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ... + /// ``` pub fn process_and_validate_instruction( &self, instruction: &Instruction, @@ -223,6 +235,13 @@ impl Mollusk { checks: &[Check], ) -> InstructionResult { let result = self.process_instruction(instruction, accounts); + + #[cfg(feature = "fuzz")] + if let Ok(dir_path) = std::env::var("EJECT_FUZZ_FIXTURES") { + fuzz::build_fixture_from_mollusk_test(self, instruction, accounts, &result, checks) + .dump_to_blob_file(&dir_path); + } + result.run_checks(checks); result } diff --git a/harness/tests/dump_fixture.rs b/harness/tests/dump_fixture.rs new file mode 100644 index 0000000..e13fd88 --- /dev/null +++ b/harness/tests/dump_fixture.rs @@ -0,0 +1,108 @@ +#![cfg(feature = "fuzz")] + +use { + mollusk_svm::{fuzz::fixture::Fixture, result::Check, Mollusk}, + solana_sdk::{ + account::AccountSharedData, feature_set::FeatureSet, instruction::Instruction, + pubkey::Pubkey, system_instruction, system_program, + }, + std::path::Path, +}; + +const EJECT_FUZZ_FIXTURES: &str = "./tests"; + +fn is_fixture_file(path: &Path) -> bool { + if path.is_file() { + let path = path.to_str().unwrap(); + if path.ends_with(".fix") { + return true; + } + } + false +} + +// Find the first fixture in the `EJECT_FUZZ_FIXTURES` directory. +fn find_fixture() -> Option { + let dir = std::fs::read_dir(EJECT_FUZZ_FIXTURES).unwrap(); + dir.filter_map(|entry| { + let entry = entry.unwrap(); + let path = entry.path(); + if is_fixture_file(&path) { + return Some(path.to_str().unwrap().to_string()); + } + None + }) + .next() +} + +// Remove all fixture files in the `EJECT_FUZZ_FIXTURES` directory. +fn clear() { + let dir = std::fs::read_dir(EJECT_FUZZ_FIXTURES).unwrap(); + for entry in dir { + let entry = entry.unwrap(); + let path = entry.path(); + if is_fixture_file(&path) { + std::fs::remove_file(path).unwrap(); + } + } +} + +fn mollusk_test() -> (Mollusk, Instruction, [(Pubkey, AccountSharedData); 2]) { + let sender = Pubkey::new_unique(); + let recipient = Pubkey::new_unique(); + + let base_lamports = 100_000_000u64; + let transfer_amount = 42_000u64; + + let mollusk = Mollusk::default(); + + let instruction = system_instruction::transfer(&sender, &recipient, transfer_amount); + let accounts = [ + ( + sender, + AccountSharedData::new(base_lamports, 0, &system_program::id()), + ), + ( + recipient, + AccountSharedData::new(base_lamports, 0, &system_program::id()), + ), + ]; + let checks = vec![ + Check::success(), + Check::account(&sender) + .lamports(base_lamports - transfer_amount) + .build(), + Check::account(&recipient) + .lamports(base_lamports + transfer_amount) + .build(), + ]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + + (mollusk, instruction, accounts) +} + +#[test] +fn test_dump() { + clear(); + std::env::set_var("EJECT_FUZZ_FIXTURES", EJECT_FUZZ_FIXTURES); + + let (mollusk, instruction, accounts) = mollusk_test(); + + let fixture_path = find_fixture().unwrap(); + let fixture = Fixture::load_from_blob_file(&fixture_path).unwrap(); + assert_eq!(fixture.input.compute_budget, mollusk.compute_budget); + assert_eq!( + FeatureSet::from(fixture.input.feature_set), + mollusk.feature_set + ); + assert_eq!(fixture.input.sysvar_context.clock, mollusk.sysvars.clock); + assert_eq!(fixture.input.sysvar_context.rent, mollusk.sysvars.rent); + assert_eq!(fixture.input.program_id, instruction.program_id); + assert_eq!(fixture.input.instruction_accounts, instruction.accounts); + assert_eq!(fixture.input.instruction_data, instruction.data); + assert_eq!(fixture.input.accounts, accounts); + + std::env::remove_var("EJECT_FUZZ_FIXTURES"); + clear(); +} From f63a6e9bade206ca955282a9c0f5d3a0c3240c14 Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 15:44:27 +0800 Subject: [PATCH 09/13] serde support for fixtures --- Cargo.lock | 1 + Cargo.toml | 1 + fuzz/Cargo.toml | 1 + fuzz/src/fixture/compute_budget.rs | 179 +++++++++++++++++++++++++++-- fuzz/src/fixture/context.rs | 9 +- fuzz/src/fixture/effects.rs | 3 +- fuzz/src/fixture/feature_set.rs | 3 +- fuzz/src/fixture/mod.rs | 3 +- fuzz/src/fixture/sysvars.rs | 3 +- harness/src/fuzz.rs | 2 +- harness/tests/dump_fixture.rs | 6 +- 11 files changed, 194 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a034b8..23088bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1588,6 +1588,7 @@ dependencies = [ "prost", "prost-build", "prost-types", + "serde", "solana-compute-budget", "solana-sdk", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index cabc3bf..29109ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ num-format = "0.4.4" prost = "0.10" prost-build = "0.10" prost-types = "0.10" +serde = "1.0.203" serde_json = "1.0.117" solana-bpf-loader-program = "2.0" solana-compute-budget = "2.0" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 79f06b5..3bf08f2 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -12,6 +12,7 @@ version = { workspace = true } bs58 = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } +serde = { workspace = true, features = ["derive"] } solana-compute-budget = { workspace = true } solana-sdk = { workspace = true } thiserror = { workspace = true } diff --git a/fuzz/src/fixture/compute_budget.rs b/fuzz/src/fixture/compute_budget.rs index e994def..f6b6a02 100644 --- a/fuzz/src/fixture/compute_budget.rs +++ b/fuzz/src/fixture/compute_budget.rs @@ -1,8 +1,66 @@ //! Compute budget for instructions. -use {super::proto, solana_compute_budget::compute_budget::ComputeBudget}; +use { + super::proto, + serde::{Deserialize, Serialize}, + solana_compute_budget::compute_budget::ComputeBudget, +}; -impl From for ComputeBudget { +#[derive(Debug, Deserialize, Serialize)] +pub struct FixtureComputeBudget { + compute_unit_limit: u64, + log_64_units: u64, + create_program_address_units: u64, + invoke_units: u64, + max_instruction_stack_depth: usize, + max_instruction_trace_length: usize, + sha256_base_cost: u64, + sha256_byte_cost: u64, + sha256_max_slices: u64, + max_call_depth: usize, + stack_frame_size: usize, + log_pubkey_units: u64, + max_cpi_instruction_size: usize, + cpi_bytes_per_unit: u64, + sysvar_base_cost: u64, + secp256k1_recover_cost: u64, + syscall_base_cost: u64, + curve25519_edwards_validate_point_cost: u64, + curve25519_edwards_add_cost: u64, + curve25519_edwards_subtract_cost: u64, + curve25519_edwards_multiply_cost: u64, + curve25519_edwards_msm_base_cost: u64, + curve25519_edwards_msm_incremental_cost: u64, + curve25519_ristretto_validate_point_cost: u64, + curve25519_ristretto_add_cost: u64, + curve25519_ristretto_subtract_cost: u64, + curve25519_ristretto_multiply_cost: u64, + curve25519_ristretto_msm_base_cost: u64, + curve25519_ristretto_msm_incremental_cost: u64, + heap_size: u32, + heap_cost: u64, + mem_op_base_cost: u64, + alt_bn128_addition_cost: u64, + alt_bn128_multiplication_cost: u64, + alt_bn128_pairing_one_pair_cost_first: u64, + alt_bn128_pairing_one_pair_cost_other: u64, + big_modular_exponentiation_cost: u64, + poseidon_cost_coefficient_a: u64, + poseidon_cost_coefficient_c: u64, + get_remaining_compute_units_cost: u64, + alt_bn128_g1_compress: u64, + alt_bn128_g1_decompress: u64, + alt_bn128_g2_compress: u64, + alt_bn128_g2_decompress: u64, +} + +impl Default for FixtureComputeBudget { + fn default() -> Self { + Self::from(&ComputeBudget::default()) + } +} + +impl From for FixtureComputeBudget { fn from(input: proto::ComputeBudget) -> Self { let proto::ComputeBudget { compute_unit_limit, @@ -57,7 +115,7 @@ impl From for ComputeBudget { let stack_frame_size = stack_frame_size as usize; let max_cpi_instruction_size = max_cpi_instruction_size as usize; - ComputeBudget { + FixtureComputeBudget { compute_unit_limit, log_64_units, create_program_address_units, @@ -106,9 +164,9 @@ impl From for ComputeBudget { } } -impl From<&ComputeBudget> for proto::ComputeBudget { - fn from(input: &ComputeBudget) -> Self { - let ComputeBudget { +impl From<&FixtureComputeBudget> for proto::ComputeBudget { + fn from(input: &FixtureComputeBudget) -> Self { + let FixtureComputeBudget { compute_unit_limit, log_64_units, create_program_address_units, @@ -154,7 +212,8 @@ impl From<&ComputeBudget> for proto::ComputeBudget { alt_bn128_g2_compress, alt_bn128_g2_decompress, } = input; - Self { + + proto::ComputeBudget { compute_unit_limit: *compute_unit_limit, log_64_units: *log_64_units, create_program_address_units: *create_program_address_units, @@ -202,3 +261,109 @@ impl From<&ComputeBudget> for proto::ComputeBudget { } } } + +impl From for ComputeBudget { + fn from(input: FixtureComputeBudget) -> Self { + ComputeBudget { + compute_unit_limit: input.compute_unit_limit, + log_64_units: input.log_64_units, + create_program_address_units: input.create_program_address_units, + invoke_units: input.invoke_units, + max_instruction_stack_depth: input.max_instruction_stack_depth, + max_instruction_trace_length: input.max_instruction_trace_length, + sha256_base_cost: input.sha256_base_cost, + sha256_byte_cost: input.sha256_byte_cost, + sha256_max_slices: input.sha256_max_slices, + max_call_depth: input.max_call_depth, + stack_frame_size: input.stack_frame_size, + log_pubkey_units: input.log_pubkey_units, + max_cpi_instruction_size: input.max_cpi_instruction_size, + cpi_bytes_per_unit: input.cpi_bytes_per_unit, + sysvar_base_cost: input.sysvar_base_cost, + secp256k1_recover_cost: input.secp256k1_recover_cost, + syscall_base_cost: input.syscall_base_cost, + curve25519_edwards_validate_point_cost: input.curve25519_edwards_validate_point_cost, + curve25519_edwards_add_cost: input.curve25519_edwards_add_cost, + curve25519_edwards_subtract_cost: input.curve25519_edwards_subtract_cost, + curve25519_edwards_multiply_cost: input.curve25519_edwards_multiply_cost, + curve25519_edwards_msm_base_cost: input.curve25519_edwards_msm_base_cost, + curve25519_edwards_msm_incremental_cost: input.curve25519_edwards_msm_incremental_cost, + curve25519_ristretto_validate_point_cost: input + .curve25519_ristretto_validate_point_cost, + curve25519_ristretto_add_cost: input.curve25519_ristretto_add_cost, + curve25519_ristretto_subtract_cost: input.curve25519_ristretto_subtract_cost, + curve25519_ristretto_multiply_cost: input.curve25519_ristretto_multiply_cost, + curve25519_ristretto_msm_base_cost: input.curve25519_ristretto_msm_base_cost, + curve25519_ristretto_msm_incremental_cost: input + .curve25519_ristretto_msm_incremental_cost, + heap_size: input.heap_size, + heap_cost: input.heap_cost, + mem_op_base_cost: input.mem_op_base_cost, + alt_bn128_addition_cost: input.alt_bn128_addition_cost, + alt_bn128_multiplication_cost: input.alt_bn128_multiplication_cost, + alt_bn128_pairing_one_pair_cost_first: input.alt_bn128_pairing_one_pair_cost_first, + alt_bn128_pairing_one_pair_cost_other: input.alt_bn128_pairing_one_pair_cost_other, + big_modular_exponentiation_cost: input.big_modular_exponentiation_cost, + poseidon_cost_coefficient_a: input.poseidon_cost_coefficient_a, + poseidon_cost_coefficient_c: input.poseidon_cost_coefficient_c, + get_remaining_compute_units_cost: input.get_remaining_compute_units_cost, + alt_bn128_g1_compress: input.alt_bn128_g1_compress, + alt_bn128_g1_decompress: input.alt_bn128_g1_decompress, + alt_bn128_g2_compress: input.alt_bn128_g2_compress, + alt_bn128_g2_decompress: input.alt_bn128_g2_decompress, + } + } +} + +impl From<&ComputeBudget> for FixtureComputeBudget { + fn from(input: &ComputeBudget) -> Self { + FixtureComputeBudget { + compute_unit_limit: input.compute_unit_limit, + log_64_units: input.log_64_units, + create_program_address_units: input.create_program_address_units, + invoke_units: input.invoke_units, + max_instruction_stack_depth: input.max_instruction_stack_depth, + max_instruction_trace_length: input.max_instruction_trace_length, + sha256_base_cost: input.sha256_base_cost, + sha256_byte_cost: input.sha256_byte_cost, + sha256_max_slices: input.sha256_max_slices, + max_call_depth: input.max_call_depth, + stack_frame_size: input.stack_frame_size, + log_pubkey_units: input.log_pubkey_units, + max_cpi_instruction_size: input.max_cpi_instruction_size, + cpi_bytes_per_unit: input.cpi_bytes_per_unit, + sysvar_base_cost: input.sysvar_base_cost, + secp256k1_recover_cost: input.secp256k1_recover_cost, + syscall_base_cost: input.syscall_base_cost, + curve25519_edwards_validate_point_cost: input.curve25519_edwards_validate_point_cost, + curve25519_edwards_add_cost: input.curve25519_edwards_add_cost, + curve25519_edwards_subtract_cost: input.curve25519_edwards_subtract_cost, + curve25519_edwards_multiply_cost: input.curve25519_edwards_multiply_cost, + curve25519_edwards_msm_base_cost: input.curve25519_edwards_msm_base_cost, + curve25519_edwards_msm_incremental_cost: input.curve25519_edwards_msm_incremental_cost, + curve25519_ristretto_validate_point_cost: input + .curve25519_ristretto_validate_point_cost, + curve25519_ristretto_add_cost: input.curve25519_ristretto_add_cost, + curve25519_ristretto_subtract_cost: input.curve25519_ristretto_subtract_cost, + curve25519_ristretto_multiply_cost: input.curve25519_ristretto_multiply_cost, + curve25519_ristretto_msm_base_cost: input.curve25519_ristretto_msm_base_cost, + curve25519_ristretto_msm_incremental_cost: input + .curve25519_ristretto_msm_incremental_cost, + heap_size: input.heap_size, + heap_cost: input.heap_cost, + mem_op_base_cost: input.mem_op_base_cost, + alt_bn128_addition_cost: input.alt_bn128_addition_cost, + alt_bn128_multiplication_cost: input.alt_bn128_multiplication_cost, + alt_bn128_pairing_one_pair_cost_first: input.alt_bn128_pairing_one_pair_cost_first, + alt_bn128_pairing_one_pair_cost_other: input.alt_bn128_pairing_one_pair_cost_other, + big_modular_exponentiation_cost: input.big_modular_exponentiation_cost, + poseidon_cost_coefficient_a: input.poseidon_cost_coefficient_a, + poseidon_cost_coefficient_c: input.poseidon_cost_coefficient_c, + get_remaining_compute_units_cost: input.get_remaining_compute_units_cost, + alt_bn128_g1_compress: input.alt_bn128_g1_compress, + alt_bn128_g1_decompress: input.alt_bn128_g1_decompress, + alt_bn128_g2_compress: input.alt_bn128_g2_compress, + alt_bn128_g2_decompress: input.alt_bn128_g2_decompress, + } + } +} diff --git a/fuzz/src/fixture/context.rs b/fuzz/src/fixture/context.rs index 679eb03..4b822fb 100644 --- a/fuzz/src/fixture/context.rs +++ b/fuzz/src/fixture/context.rs @@ -3,9 +3,10 @@ use { super::{ - error::FixtureError, feature_set::FixtureFeatureSet, proto, sysvars::FixtureSysvarContext, + compute_budget::FixtureComputeBudget, error::FixtureError, feature_set::FixtureFeatureSet, + proto, sysvars::FixtureSysvarContext, }, - solana_compute_budget::compute_budget::ComputeBudget, + serde::{Deserialize, Serialize}, solana_sdk::{ account::{AccountSharedData, ReadableAccount}, instruction::AccountMeta, @@ -14,10 +15,10 @@ use { }; /// Instruction context fixture. -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct FixtureContext { /// The compute budget to use for the simulation. - pub compute_budget: ComputeBudget, + pub compute_budget: FixtureComputeBudget, /// The feature set to use for the simulation. pub feature_set: FixtureFeatureSet, /// The sysvar context to use for the simulation. diff --git a/fuzz/src/fixture/effects.rs b/fuzz/src/fixture/effects.rs index d1a6876..431a720 100644 --- a/fuzz/src/fixture/effects.rs +++ b/fuzz/src/fixture/effects.rs @@ -2,11 +2,12 @@ use { super::{error::FixtureError, proto}, + serde::{Deserialize, Serialize}, solana_sdk::{account::AccountSharedData, pubkey::Pubkey}, }; /// Represents the effects of a single instruction. -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct FixtureEffects { /// Compute units consumed by the instruction. pub compute_units_consumed: u64, diff --git a/fuzz/src/fixture/feature_set.rs b/fuzz/src/fixture/feature_set.rs index 6c25170..5c7af17 100644 --- a/fuzz/src/fixture/feature_set.rs +++ b/fuzz/src/fixture/feature_set.rs @@ -2,6 +2,7 @@ use { super::proto, + serde::{Deserialize, Serialize}, solana_sdk::{feature_set::*, pubkey::Pubkey}, }; @@ -22,7 +23,7 @@ fn create_feature_set<'a>( feature_set } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct FixtureFeatureSet { pub features: Vec, } diff --git a/fuzz/src/fixture/mod.rs b/fuzz/src/fixture/mod.rs index 4c0c27b..603d65e 100644 --- a/fuzz/src/fixture/mod.rs +++ b/fuzz/src/fixture/mod.rs @@ -17,6 +17,7 @@ use { effects::FixtureEffects, error::FixtureError, prost::Message, + serde::{Deserialize, Serialize}, std::{ fs::{self, File}, io::{Read, Write}, @@ -26,7 +27,7 @@ use { /// A fixture for invoking a single instruction against a simulated Solana /// program runtime environment, for a given program. -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct Fixture { /// The fixture inputs. pub input: FixtureContext, diff --git a/fuzz/src/fixture/sysvars.rs b/fuzz/src/fixture/sysvars.rs index efe1db9..61ac6d7 100644 --- a/fuzz/src/fixture/sysvars.rs +++ b/fuzz/src/fixture/sysvars.rs @@ -2,6 +2,7 @@ use { super::{error::FixtureError, proto}, + serde::{Deserialize, Serialize}, solana_sdk::{ clock::Clock, epoch_rewards::EpochRewards, @@ -27,7 +28,7 @@ fn try_read_u128(bytes: &[u8]) -> Result { } /// A fixture containing the Solana runtime sysvars. -#[derive(Debug, Default)] +#[derive(Debug, Default, Deserialize, Serialize)] pub struct FixtureSysvarContext { /// `Clock` sysvar. pub clock: Clock, diff --git a/harness/src/fuzz.rs b/harness/src/fuzz.rs index 9178985..196fdff 100644 --- a/harness/src/fuzz.rs +++ b/harness/src/fuzz.rs @@ -76,7 +76,7 @@ fn build_fixture_context( let accounts = accounts.to_vec(); FixtureContext { - compute_budget: *compute_budget, + compute_budget: compute_budget.into(), feature_set: feature_set.into(), sysvar_context: sysvars.into(), program_id: instruction.program_id, diff --git a/harness/tests/dump_fixture.rs b/harness/tests/dump_fixture.rs index e13fd88..4f708f8 100644 --- a/harness/tests/dump_fixture.rs +++ b/harness/tests/dump_fixture.rs @@ -2,6 +2,7 @@ use { mollusk_svm::{fuzz::fixture::Fixture, result::Check, Mollusk}, + solana_compute_budget::compute_budget::ComputeBudget, solana_sdk::{ account::AccountSharedData, feature_set::FeatureSet, instruction::Instruction, pubkey::Pubkey, system_instruction, system_program, @@ -91,7 +92,10 @@ fn test_dump() { let fixture_path = find_fixture().unwrap(); let fixture = Fixture::load_from_blob_file(&fixture_path).unwrap(); - assert_eq!(fixture.input.compute_budget, mollusk.compute_budget); + assert_eq!( + ComputeBudget::from(fixture.input.compute_budget), + mollusk.compute_budget + ); assert_eq!( FeatureSet::from(fixture.input.feature_set), mollusk.feature_set From c188686be78b020488af878d2150afe740e44bd4 Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 17:01:50 +0800 Subject: [PATCH 10/13] helper to load fixtures from a JSON file --- Cargo.lock | 1 + fuzz/Cargo.toml | 1 + fuzz/src/fixture/error.rs | 3 +++ fuzz/src/fixture/mod.rs | 12 ++++++++++++ 4 files changed, 17 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 23088bb..c5b4d80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1589,6 +1589,7 @@ dependencies = [ "prost-build", "prost-types", "serde", + "serde_json", "solana-compute-budget", "solana-sdk", "thiserror", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 3bf08f2..3031bb0 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -13,6 +13,7 @@ bs58 = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } solana-compute-budget = { workspace = true } solana-sdk = { workspace = true } thiserror = { workspace = true } diff --git a/fuzz/src/fixture/error.rs b/fuzz/src/fixture/error.rs index cecf011..58e24f0 100644 --- a/fuzz/src/fixture/error.rs +++ b/fuzz/src/fixture/error.rs @@ -31,4 +31,7 @@ pub enum FixtureError { /// The output fixture is invalid. #[error("Invalid fixture output")] InvalidFixtureOutput, + /// The provided JSON fixture is invalid. + #[error("Invalid JSON fixture")] + InvalidJsonFixture, } diff --git a/fuzz/src/fixture/mod.rs b/fuzz/src/fixture/mod.rs index 603d65e..be330fb 100644 --- a/fuzz/src/fixture/mod.rs +++ b/fuzz/src/fixture/mod.rs @@ -97,4 +97,16 @@ impl Fixture { .expect("Failed to read fixture file"); Self::decode(&buf) } + + /// Loads a `Fixture` from a JSON file. + pub fn load_from_json_file(file_path: &str) -> Result { + if !file_path.ends_with(".json") { + panic!("Invalid fixture file extension: {}", file_path); + } + let mut file = File::open(file_path).expect("Failed to open fixture file"); + let mut json = String::new(); + file.read_to_string(&mut json) + .expect("Failed to read fixture file"); + serde_json::from_str(&json).map_err(|_| FixtureError::InvalidJsonFixture) + } } From 06cde0d67dcf0b8ef30708ace87ff3b833813be7 Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 17:02:29 +0800 Subject: [PATCH 11/13] helper to dump fixtures into a JSON file --- fuzz/src/fixture/mod.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/fuzz/src/fixture/mod.rs b/fuzz/src/fixture/mod.rs index be330fb..b917d02 100644 --- a/fuzz/src/fixture/mod.rs +++ b/fuzz/src/fixture/mod.rs @@ -86,6 +86,19 @@ impl Fixture { .expect("Failed to write fixture to file"); } + /// Dumps the `Fixture` to a JSON file. + /// The file name is a hash of the fixture with the `.json` extension. + pub fn dump_to_json_file(&self, dir_path: &str) { + let json = serde_json::to_string_pretty(&self).expect("Failed to serialize fixture"); + let hash = solana_sdk::hash::hash(json.as_bytes()); + let file_name = format!("instr-{}.json", bs58::encode(hash).into_string()); + fs::create_dir_all(dir_path).expect("Failed to create directory"); + let file_path = Path::new(dir_path).join(file_name); + let mut file = File::create(file_path).unwrap(); + file.write_all(json.as_bytes()) + .expect("Failed to write fixture to file"); + } + /// Loads a `Fixture` from a protobuf binary blob file. pub fn load_from_blob_file(file_path: &str) -> Result { if !file_path.ends_with(".fix") { From e478877329e152175d58aa7de968efd99da57e96 Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 17:24:02 +0800 Subject: [PATCH 12/13] harness: add json support for `fuzz` --- harness/src/lib.rs | 24 +++++++- harness/tests/dump_fixture.rs | 101 +++++++++++++++++++++++++--------- 2 files changed, 95 insertions(+), 30 deletions(-) diff --git a/harness/src/lib.rs b/harness/src/lib.rs index 80480f6..1ece321 100644 --- a/harness/src/lib.rs +++ b/harness/src/lib.rs @@ -228,6 +228,9 @@ impl Mollusk { /// ```ignore /// EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ... /// ``` + /// + /// You can also provide `EJECT_FUZZ_FIXTURES_JSON` to write the fixture in + /// JSON format. pub fn process_and_validate_instruction( &self, instruction: &Instruction, @@ -237,9 +240,24 @@ impl Mollusk { let result = self.process_instruction(instruction, accounts); #[cfg(feature = "fuzz")] - if let Ok(dir_path) = std::env::var("EJECT_FUZZ_FIXTURES") { - fuzz::build_fixture_from_mollusk_test(self, instruction, accounts, &result, checks) - .dump_to_blob_file(&dir_path); + { + let blob_dir = std::env::var("EJECT_FUZZ_FIXTURES").ok(); + let json_dir = std::env::var("EJECT_FUZZ_FIXTURES_JSON").ok(); + if blob_dir.is_some() || json_dir.is_some() { + let fixture = fuzz::build_fixture_from_mollusk_test( + self, + instruction, + accounts, + &result, + checks, + ); + if let Some(blob_dir) = blob_dir { + fixture.dump_to_blob_file(&blob_dir); + } + if let Some(json_dir) = json_dir { + fixture.dump_to_json_file(&json_dir); + } + } } result.run_checks(checks); diff --git a/harness/tests/dump_fixture.rs b/harness/tests/dump_fixture.rs index 4f708f8..8b7557b 100644 --- a/harness/tests/dump_fixture.rs +++ b/harness/tests/dump_fixture.rs @@ -12,10 +12,24 @@ use { const EJECT_FUZZ_FIXTURES: &str = "./tests"; -fn is_fixture_file(path: &Path) -> bool { +enum FileType { + Blob, + Json, +} + +impl FileType { + fn extension(&self) -> &'static str { + match self { + Self::Blob => ".fix", + Self::Json => ".json", + } + } +} + +fn is_fixture_file(path: &Path, file_type: &FileType) -> bool { if path.is_file() { let path = path.to_str().unwrap(); - if path.ends_with(".fix") { + if path.ends_with(file_type.extension()) { return true; } } @@ -23,12 +37,12 @@ fn is_fixture_file(path: &Path) -> bool { } // Find the first fixture in the `EJECT_FUZZ_FIXTURES` directory. -fn find_fixture() -> Option { +fn find_fixture(file_type: &FileType) -> Option { let dir = std::fs::read_dir(EJECT_FUZZ_FIXTURES).unwrap(); dir.filter_map(|entry| { let entry = entry.unwrap(); let path = entry.path(); - if is_fixture_file(&path) { + if is_fixture_file(&path, file_type) { return Some(path.to_str().unwrap().to_string()); } None @@ -42,7 +56,7 @@ fn clear() { for entry in dir { let entry = entry.unwrap(); let path = entry.path(); - if is_fixture_file(&path) { + if is_fixture_file(&path, &FileType::Blob) || is_fixture_file(&path, &FileType::Json) { std::fs::remove_file(path).unwrap(); } } @@ -86,27 +100,60 @@ fn mollusk_test() -> (Mollusk, Instruction, [(Pubkey, AccountSharedData); 2]) { #[test] fn test_dump() { clear(); - std::env::set_var("EJECT_FUZZ_FIXTURES", EJECT_FUZZ_FIXTURES); - - let (mollusk, instruction, accounts) = mollusk_test(); - - let fixture_path = find_fixture().unwrap(); - let fixture = Fixture::load_from_blob_file(&fixture_path).unwrap(); - assert_eq!( - ComputeBudget::from(fixture.input.compute_budget), - mollusk.compute_budget - ); - assert_eq!( - FeatureSet::from(fixture.input.feature_set), - mollusk.feature_set - ); - assert_eq!(fixture.input.sysvar_context.clock, mollusk.sysvars.clock); - assert_eq!(fixture.input.sysvar_context.rent, mollusk.sysvars.rent); - assert_eq!(fixture.input.program_id, instruction.program_id); - assert_eq!(fixture.input.instruction_accounts, instruction.accounts); - assert_eq!(fixture.input.instruction_data, instruction.data); - assert_eq!(fixture.input.accounts, accounts); - - std::env::remove_var("EJECT_FUZZ_FIXTURES"); + + // First try protobuf. + { + std::env::set_var("EJECT_FUZZ_FIXTURES", EJECT_FUZZ_FIXTURES); + + let (mollusk, instruction, accounts) = mollusk_test(); + + let fixture_path = find_fixture(&FileType::Blob).unwrap(); + let fixture = Fixture::load_from_blob_file(&fixture_path).unwrap(); + + assert_eq!( + ComputeBudget::from(fixture.input.compute_budget), + mollusk.compute_budget + ); + assert_eq!( + FeatureSet::from(fixture.input.feature_set), + mollusk.feature_set + ); + assert_eq!(fixture.input.sysvar_context.clock, mollusk.sysvars.clock); + assert_eq!(fixture.input.sysvar_context.rent, mollusk.sysvars.rent); + assert_eq!(fixture.input.program_id, instruction.program_id); + assert_eq!(fixture.input.instruction_accounts, instruction.accounts); + assert_eq!(fixture.input.instruction_data, instruction.data); + assert_eq!(fixture.input.accounts, accounts); + + std::env::remove_var("EJECT_FUZZ_FIXTURES"); + } + + // Now try JSON. + { + std::env::set_var("EJECT_FUZZ_FIXTURES_JSON", EJECT_FUZZ_FIXTURES); + + let (mollusk, instruction, accounts) = mollusk_test(); + + let fixture_path = find_fixture(&FileType::Json).unwrap(); + let fixture = Fixture::load_from_json_file(&fixture_path).unwrap(); + + assert_eq!( + ComputeBudget::from(fixture.input.compute_budget), + mollusk.compute_budget + ); + assert_eq!( + FeatureSet::from(fixture.input.feature_set), + mollusk.feature_set + ); + assert_eq!(fixture.input.sysvar_context.clock, mollusk.sysvars.clock); + assert_eq!(fixture.input.sysvar_context.rent, mollusk.sysvars.rent); + assert_eq!(fixture.input.program_id, instruction.program_id); + assert_eq!(fixture.input.instruction_accounts, instruction.accounts); + assert_eq!(fixture.input.instruction_data, instruction.data); + assert_eq!(fixture.input.accounts, accounts); + + std::env::remove_var("EJECT_FUZZ_FIXTURES_JSON"); + } + clear(); } From 4cff4345218ecea22cdf13e6b5602a0a55a9ab9b Mon Sep 17 00:00:00 2001 From: Joe Caulfield Date: Thu, 10 Oct 2024 15:44:33 +0800 Subject: [PATCH 13/13] update CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2050d2c..9807288 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -65,4 +65,4 @@ jobs: cargo build-sbf --manifest-path test-programs/cpi-target/Cargo.toml cargo build-sbf --manifest-path test-programs/primary/Cargo.toml - name: Test - run: cargo test + run: cargo test --all-features