Skip to content

Commit 7ad9d61

Browse files
authored
harness: add rent-exemption account check (#112)
1 parent 7d5e16b commit 7ad9d61

File tree

3 files changed

+110
-16
lines changed

3 files changed

+110
-16
lines changed

harness/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ use {
387387
crate::{
388388
compile_accounts::CompiledAccounts,
389389
program::ProgramCache,
390-
result::{Check, Config, InstructionResult},
390+
result::{Check, CheckContext, Config, InstructionResult},
391391
sysvar::Sysvars,
392392
},
393393
agave_feature_set::FeatureSet,
@@ -457,6 +457,12 @@ impl Default for Mollusk {
457457
}
458458
}
459459

460+
impl CheckContext for Mollusk {
461+
fn is_rent_exempt(&self, lamports: u64, space: usize) -> bool {
462+
self.sysvars.rent.is_exempt(lamports, space)
463+
}
464+
}
465+
460466
impl Mollusk {
461467
/// Create a new Mollusk instance containing the provided program.
462468
///
@@ -681,7 +687,7 @@ impl Mollusk {
681687
#[cfg(any(feature = "fuzz", feature = "fuzz-fd"))]
682688
fuzz::generate_fixtures_from_mollusk_test(self, instruction, accounts, &result);
683689

684-
result.run_checks_with_config(checks, &self.config);
690+
result.run_checks(checks, &self.config, self);
685691
result
686692
}
687693

harness/src/result.rs

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use {
55
solana_instruction::error::InstructionError,
66
solana_program_error::ProgramError,
77
solana_pubkey::Pubkey,
8+
solana_rent::Rent,
89
};
910

1011
macro_rules! compare {
@@ -122,6 +123,19 @@ impl Default for Config {
122123
}
123124
}
124125

126+
/// A trait for providing context to the checks.
127+
///
128+
/// Developers who run checks on standalone results, rather than passing checks
129+
/// directly to methods like `Mollusk::process_and_validate_instruction`, may
130+
/// wish to customize the context in which the checks are run. For example,
131+
/// one may wish to evaluate resulting account lamports with a custom `Rent`
132+
/// configuration. This trait allows such customization.
133+
pub trait CheckContext {
134+
fn is_rent_exempt(&self, lamports: u64, space: usize) -> bool {
135+
Rent::default().is_exempt(lamports, space)
136+
}
137+
}
138+
125139
impl InstructionResult {
126140
/// Get an account from the resulting accounts by its pubkey.
127141
pub fn get_account(&self, pubkey: &Pubkey) -> Option<&Account> {
@@ -131,8 +145,17 @@ impl InstructionResult {
131145
.map(|(_, a)| a)
132146
}
133147

134-
/// Perform checks on the instruction result.
135-
pub fn run_checks_with_config(&self, checks: &[Check], config: &Config) -> bool {
148+
/// Perform checks on the instruction result with a custom context.
149+
/// See `CheckContext` for more details.
150+
///
151+
/// Note: `Mollusk` implements `CheckContext`, in case you don't want to
152+
/// define a custom context.
153+
pub fn run_checks<C: CheckContext>(
154+
&self,
155+
checks: &[Check],
156+
config: &Config,
157+
context: &C,
158+
) -> bool {
136159
let c = config;
137160
let mut pass = true;
138161
for check in checks {
@@ -199,6 +222,17 @@ impl InstructionResult {
199222
resulting_account == &Account::default(),
200223
);
201224
}
225+
AccountStateCheck::RentExempt => {
226+
pass &= compare!(
227+
c,
228+
"account_rent_exempt",
229+
true,
230+
context.is_rent_exempt(
231+
resulting_account.lamports,
232+
resulting_account.data.len()
233+
),
234+
);
235+
}
202236
}
203237
}
204238
if let Some((offset, check_data_slice)) = account.check_data_slice {
@@ -225,17 +259,6 @@ impl InstructionResult {
225259
pass
226260
}
227261

228-
/// Perform checks on the instruction result, panicking on any mismatches.
229-
pub fn run_checks(&self, checks: &[Check]) {
230-
self.run_checks_with_config(
231-
checks,
232-
&Config {
233-
panic: true,
234-
verbose: true,
235-
},
236-
);
237-
}
238-
239262
pub(crate) fn absorb(&mut self, other: Self) {
240263
self.compute_units_consumed += other.compute_units_consumed;
241264
self.execution_time += other.execution_time;
@@ -484,6 +507,7 @@ impl<'a> Check<'a> {
484507

485508
enum AccountStateCheck {
486509
Closed,
510+
RentExempt,
487511
}
488512

489513
struct AccountCheck<'a> {
@@ -548,6 +572,11 @@ impl<'a> AccountCheckBuilder<'a> {
548572
self
549573
}
550574

575+
pub fn rent_exempt(mut self) -> Self {
576+
self.check.check_state = Some(AccountStateCheck::RentExempt);
577+
self
578+
}
579+
551580
pub fn space(mut self, space: usize) -> Self {
552581
self.check.check_space = Some(space);
553582
self

harness/tests/bpf_program.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use {
22
mollusk_svm::{
33
program::{create_program_account_loader_v3, keyed_account_for_system_program},
4-
result::Check,
4+
result::{Check, CheckContext},
55
Mollusk,
66
},
77
solana_account::Account,
88
solana_instruction::{error::InstructionError, AccountMeta, Instruction},
99
solana_program_error::ProgramError,
1010
solana_pubkey::Pubkey,
11+
solana_rent::Rent,
1112
solana_system_interface::error::SystemError,
1213
};
1314

@@ -401,3 +402,61 @@ fn test_account_dedupe() {
401402
);
402403
}
403404
}
405+
406+
#[test]
407+
fn test_account_checks_rent_exemption() {
408+
std::env::set_var("SBF_OUT_DIR", "../target/deploy");
409+
410+
let program_id = Pubkey::new_unique();
411+
412+
let mut mollusk = Mollusk::new(&program_id, "test_program_primary");
413+
mollusk.config.panic = false; // Don't panic, so we can evaluate failing checks.
414+
415+
let key = Pubkey::new_unique();
416+
417+
let data_len = 8;
418+
let data = vec![4; data_len];
419+
420+
let rent_exempt_lamports = mollusk.sysvars.rent.minimum_balance(data_len);
421+
let not_rent_exempt_lamports = rent_exempt_lamports - 1;
422+
423+
struct TestCheckContext<'a> {
424+
rent: &'a Rent,
425+
}
426+
427+
impl CheckContext for TestCheckContext<'_> {
428+
fn is_rent_exempt(&self, lamports: u64, space: usize) -> bool {
429+
self.rent.is_exempt(lamports, space)
430+
}
431+
}
432+
433+
let get_result = |lamports: u64| {
434+
mollusk
435+
.process_and_validate_instruction(
436+
&Instruction::new_with_bytes(
437+
program_id,
438+
&{
439+
let mut instruction_data = vec![1]; // `WriteData`
440+
instruction_data.extend_from_slice(&data);
441+
instruction_data
442+
},
443+
vec![AccountMeta::new(key, true)],
444+
),
445+
&[(key, Account::new(lamports, data_len, &program_id))],
446+
&[Check::success()], // It should still pass.
447+
)
448+
.run_checks(
449+
&[Check::account(&key).rent_exempt().build()],
450+
&mollusk.config,
451+
&TestCheckContext {
452+
rent: &mollusk.sysvars.rent,
453+
},
454+
)
455+
};
456+
457+
// Fail not rent exempt.
458+
assert!(!get_result(not_rent_exempt_lamports));
459+
460+
// Success rent exempt.
461+
assert!(get_result(rent_exempt_lamports));
462+
}

0 commit comments

Comments
 (0)