Skip to content

Commit f01db90

Browse files
authored
support fuzz fixtures in ix chains (#79)
1 parent 5fe33b0 commit f01db90

File tree

5 files changed

+190
-46
lines changed

5 files changed

+190
-46
lines changed

harness/src/lib.rs

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
//! * `process_instruction`: Process an instruction and return the result.
2727
//! * `process_and_validate_instruction`: Process an instruction and perform a
2828
//! series of checks on the result, panicking if any checks fail.
29+
//! * `process_instruction_chain`: Process a chain of instructions and return
30+
//! the result.
31+
//! * `process_and_validate_instruction_chain`: Process a chain of instructions
32+
//! and perform a series of checks on each result, panicking if any checks
33+
//! fail.
2934
3035
mod accounts;
3136
pub mod file;
@@ -273,11 +278,7 @@ impl Mollusk {
273278
for instruction in instructions {
274279
let this_result = self.process_instruction(instruction, &result.resulting_accounts);
275280

276-
result.compute_units_consumed += this_result.compute_units_consumed;
277-
result.execution_time += this_result.execution_time;
278-
result.program_result = this_result.program_result;
279-
result.raw_result = this_result.raw_result;
280-
result.resulting_accounts = this_result.resulting_accounts;
281+
result.absorb(this_result);
281282

282283
if result.program_result.is_err() {
283284
break;
@@ -327,17 +328,51 @@ impl Mollusk {
327328
/// Process a chain of instructions using the minified Solana Virtual
328329
/// Machine (SVM) environment, then perform checks on the result.
329330
/// Panics if any checks fail.
331+
///
332+
/// For `fuzz` feature only:
333+
///
334+
/// Similar to `process_and_validate_instruction`, if the
335+
/// `EJECT_FUZZ_FIXTURES` environment variable is set, this function will
336+
/// convert the provided test to a set of fuzz fixtures - each of which
337+
/// corresponds to a single instruction in the chain - and write them to
338+
/// the provided directory.
339+
///
340+
/// ```ignore
341+
/// EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ...
342+
/// ```
343+
///
344+
/// You can also provide `EJECT_FUZZ_FIXTURES_JSON` to write the fixture in
345+
/// JSON format.
346+
///
347+
/// The `fuzz-fd` feature works the same way, but the variables require
348+
/// the `_FD` suffix, in case both features are active together
349+
/// (ie. `EJECT_FUZZ_FIXTURES_FD`). This will generate Firedancer fuzzing
350+
/// fixtures, which are structured a bit differently than Mollusk's own
351+
/// protobuf layouts.
330352
pub fn process_and_validate_instruction_chain(
331353
&self,
332-
instructions: &[Instruction],
354+
instructions: &[(&Instruction, &[Check])],
333355
accounts: &[(Pubkey, Account)],
334-
checks: &[Check],
335356
) -> InstructionResult {
336-
let result = self.process_instruction_chain(instructions, accounts);
357+
let mut result = InstructionResult {
358+
resulting_accounts: accounts.to_vec(),
359+
..Default::default()
360+
};
337361

338-
// No fuzz support yet...
362+
for (instruction, checks) in instructions.iter() {
363+
let this_result = self.process_and_validate_instruction(
364+
instruction,
365+
&result.resulting_accounts,
366+
checks,
367+
);
368+
369+
result.absorb(this_result);
370+
371+
if result.program_result.is_err() {
372+
break;
373+
}
374+
}
339375

340-
result.run_checks(checks);
341376
result
342377
}
343378

harness/src/result.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,15 @@ impl InstructionResult {
207207
}
208208
}
209209

210+
pub(crate) fn absorb(&mut self, other: Self) {
211+
self.compute_units_consumed += other.compute_units_consumed;
212+
self.execution_time += other.execution_time;
213+
self.program_result = other.program_result;
214+
self.raw_result = other.raw_result;
215+
self.return_data = other.return_data;
216+
self.resulting_accounts = other.resulting_accounts;
217+
}
218+
210219
/// Compare an `InstructionResult` against another `InstructionResult`,
211220
/// panicking on any mismatches.
212221
pub fn compare(&self, b: &Self) {

harness/tests/dump_fixture.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,34 @@ fn test_dump_mollusk() {
195195
// Ensure both files have the same name.
196196
assert_filenames_match(&blob_fixture_path, &json_fixture_path);
197197

198+
// Now check instruction chains.
199+
clear(EJECT_FUZZ_FIXTURES);
200+
201+
setup.mollusk.process_and_validate_instruction_chain(
202+
&[
203+
(&setup.instruction, &[]),
204+
(&setup.instruction, &[]),
205+
(&setup.instruction, &[]),
206+
],
207+
&setup.accounts,
208+
);
209+
210+
// Ensure there are three of each fixture type in the target directory.
211+
let dir = std::fs::read_dir(EJECT_FUZZ_FIXTURES).unwrap();
212+
let mut count_blob = 0;
213+
let mut count_json = 0;
214+
for entry in dir {
215+
let path = entry.unwrap().path();
216+
if is_fixture_file(&path, &FileType::Blob) {
217+
count_blob += 1;
218+
}
219+
if is_fixture_file(&path, &FileType::Json) {
220+
count_json += 1;
221+
}
222+
}
223+
assert_eq!(count_blob, 3);
224+
assert_eq!(count_json, 3);
225+
198226
std::env::remove_var("EJECT_FUZZ_FIXTURES");
199227
std::env::remove_var("EJECT_FUZZ_FIXTURES_JSON");
200228
clear(EJECT_FUZZ_FIXTURES);
@@ -235,6 +263,34 @@ fn test_dump_firedancer() {
235263
// Ensure both files have the same name.
236264
assert_filenames_match(&blob_fixture_path, &json_fixture_path);
237265

266+
// Now check instruction chains.
267+
clear(EJECT_FUZZ_FIXTURES_FD);
268+
269+
setup.mollusk.process_and_validate_instruction_chain(
270+
&[
271+
(&setup.instruction, &[]),
272+
(&setup.instruction, &[]),
273+
(&setup.instruction, &[]),
274+
],
275+
&setup.accounts,
276+
);
277+
278+
// Ensure there are three of each fixture type in the target directory.
279+
let dir = std::fs::read_dir(EJECT_FUZZ_FIXTURES_FD).unwrap();
280+
let mut count_blob = 0;
281+
let mut count_json = 0;
282+
for entry in dir {
283+
let path = entry.unwrap().path();
284+
if is_fixture_file(&path, &FileType::Blob) {
285+
count_blob += 1;
286+
}
287+
if is_fixture_file(&path, &FileType::Json) {
288+
count_json += 1;
289+
}
290+
}
291+
assert_eq!(count_blob, 3);
292+
assert_eq!(count_json, 3);
293+
238294
std::env::remove_var("EJECT_FUZZ_FIXTURES_FD");
239295
std::env::remove_var("EJECT_FUZZ_FIXTURES_JSON_FD");
240296
clear(EJECT_FUZZ_FIXTURES_FD);

harness/tests/instruction_chain.rs

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,70 @@ fn test_transfers() {
3030

3131
mollusk.process_and_validate_instruction_chain(
3232
&[
33-
system_instruction::transfer(&alice, &bob, alice_to_bob),
34-
system_instruction::transfer(&bob, &carol, bob_to_carol),
35-
system_instruction::transfer(&bob, &dave, bob_to_dave),
33+
(
34+
// 0: Alice to Bob
35+
&system_instruction::transfer(&alice, &bob, alice_to_bob),
36+
&[
37+
Check::success(),
38+
Check::account(&alice)
39+
.lamports(starting_lamports - alice_to_bob) // Alice pays
40+
.build(),
41+
Check::account(&bob)
42+
.lamports(starting_lamports + alice_to_bob) // Bob receives
43+
.build(),
44+
Check::account(&carol)
45+
.lamports(starting_lamports) // Unchanged
46+
.build(),
47+
Check::account(&dave)
48+
.lamports(starting_lamports) // Unchanged
49+
.build(),
50+
],
51+
),
52+
(
53+
// 1: Bob to Carol
54+
&system_instruction::transfer(&bob, &carol, bob_to_carol),
55+
&[
56+
Check::success(),
57+
Check::account(&alice)
58+
.lamports(starting_lamports - alice_to_bob) // Unchanged
59+
.build(),
60+
Check::account(&bob)
61+
.lamports(starting_lamports + alice_to_bob - bob_to_carol) // Bob pays
62+
.build(),
63+
Check::account(&carol)
64+
.lamports(starting_lamports + bob_to_carol) // Carol receives
65+
.build(),
66+
Check::account(&dave)
67+
.lamports(starting_lamports) // Unchanged
68+
.build(),
69+
],
70+
),
71+
(
72+
// 2: Bob to Dave
73+
&system_instruction::transfer(&bob, &dave, bob_to_dave),
74+
&[
75+
Check::success(),
76+
Check::account(&alice)
77+
.lamports(starting_lamports - alice_to_bob) // Unchanged
78+
.build(),
79+
Check::account(&bob)
80+
.lamports(starting_lamports + alice_to_bob - bob_to_carol - bob_to_dave) // Bob pays
81+
.build(),
82+
Check::account(&carol)
83+
.lamports(starting_lamports + bob_to_carol) // Unchanged
84+
.build(),
85+
Check::account(&dave)
86+
.lamports(starting_lamports + bob_to_dave) // Dave receives
87+
.build(),
88+
],
89+
),
3690
],
3791
&[
3892
(alice, system_account_with_lamports(starting_lamports)),
3993
(bob, system_account_with_lamports(starting_lamports)),
4094
(carol, system_account_with_lamports(starting_lamports)),
4195
(dave, system_account_with_lamports(starting_lamports)),
4296
],
43-
&[
44-
Check::success(),
45-
Check::account(&alice)
46-
.lamports(starting_lamports - alice_to_bob)
47-
.build(),
48-
Check::account(&bob)
49-
.lamports(starting_lamports + alice_to_bob - bob_to_carol - bob_to_dave)
50-
.build(),
51-
Check::account(&carol)
52-
.lamports(starting_lamports + bob_to_carol)
53-
.build(),
54-
Check::account(&dave)
55-
.lamports(starting_lamports + bob_to_dave)
56-
.build(),
57-
],
5897
);
5998
}
6099

@@ -117,15 +156,27 @@ fn test_mixed() {
117156

118157
mollusk.process_and_validate_instruction_chain(
119158
&[
120-
ix_transfer_to_1,
121-
ix_transfer_to_2,
122-
ix_allocate_1,
123-
ix_allocate_2,
124-
ix_assign_1,
125-
ix_assign_2,
126-
ix_write_data_to_1,
127-
ix_write_data_to_2,
128-
ix_close_1,
159+
(&ix_transfer_to_1, &[]),
160+
(&ix_transfer_to_2, &[]),
161+
(&ix_allocate_1, &[]),
162+
(&ix_allocate_2, &[]),
163+
(&ix_assign_1, &[]),
164+
(&ix_assign_2, &[]),
165+
(&ix_write_data_to_1, &[]),
166+
(&ix_write_data_to_2, &[]),
167+
(
168+
&ix_close_1,
169+
// Just check the final result.
170+
&[
171+
Check::success(),
172+
Check::account(&target1).closed().build(),
173+
Check::account(&target2)
174+
.data(data)
175+
.lamports(lamports)
176+
.owner(&program_id)
177+
.build(),
178+
],
179+
),
129180
],
130181
&[
131182
(payer, system_account_with_lamports(lamports * 4)),
@@ -134,14 +185,5 @@ fn test_mixed() {
134185
(incinerator::id(), Account::default()),
135186
keyed_account_for_system_program(),
136187
],
137-
&[
138-
Check::success(),
139-
Check::account(&target1).closed().build(),
140-
Check::account(&target2)
141-
.data(data)
142-
.lamports(lamports)
143-
.owner(&program_id)
144-
.build(),
145-
],
146188
);
147189
}

scripts/prepublish.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#!/bin/bash
22

3+
set -e
4+
35
agave-install init 2.1.0
46
rm -rf target
57
cargo build

0 commit comments

Comments
 (0)